[Perf] (MG_Backend): make DirectGLES program switches remember their own bindings

mc_use_program cycles programs whose texture bindings never change, yet
every switch re-walked the units. Six fixes, one theme: a switch back to
a known program should find its own state waiting.

Per-program 4-entry resolved-texture-binding memo (round-robin, shadow
memcmp on hit) skips the unit walk when a program returns with its
bindings intact. The whole sampler-uniform pass in
BindCurrentProgramWithResources is memoized per program twin behind
(context, unitBindingsEpoch, samplingGeneration, backendStateVersion,
textureContextGeneration) plus a per-sampled-unit sampler-shadow row
compare, invalidated on relink/backend rebuild; the
BindCurrentUnitSamplers walk sits behind the same keys. Every unit
assignment, sampler-parameter change and bind path was verified to bump
one of those inputs.

UboRingAllocate's common path is now a generation check, a power-of-two
mask, an overrun check and a head bump - the duplicate availability
probe, frame-mark retirement and divisions moved to the wrap slow path.
The per-context framebuffer binding slots (the frontend getter
linear-scans per call) are cached as direct pointers - slots are
by-value members of GLContext, so the pointers are stable by
construction - feeding SyncCurrentFBO, SyncNeccessaryTextures and the
broadcast memo; BindCurrentFBO's per-draw registry hash Find became a
TwinLookupMemo probe. The VAO config-version cold-line load is hoisted
to the top of PrepareForDraw to overlap its miss.

Quiet-box load-gated 6-round order-alternating A/B, all nine cases,
both backends: use_program -27.7%, vanilla_draw -16.2%, ubo_range
-13.4%, pass_switch -11.1%, sampler_churn -10.7%, state_toggle -9.2%,
sodium_multidraw -5.3%, rest flat. No regression on either backend
(magma's one matrix flag disproved by isolated re-runs against
byte-identical DirectVulkan sources). Unit tests 421/421.
This commit is contained in:
BZLZHH
2026-08-06 14:07:49 -04:00
parent 8f2b766b56
commit 25a8f51db5
3 changed files with 332 additions and 84 deletions
+242 -79
View File
@@ -119,6 +119,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_vaoTwinLookupMemo;
static TwinLookupMemo<MG_State::GLState::ProgramObject, PrgramImpl::BackendProgramObjectImpl, 8>
g_programTwinLookupMemo;
// Framebuffers qualify for the same memo (see the class comment's registry
// invariants: g_backendFramebufferObjects only GetOrCreate-resets an expired
// owner's twin and never erases/replaces a live one). Apps bind a handful of
// FBOs; 64 slots is plenty.
static TwinLookupMemo<MG_State::GLState::FramebufferObject, FramebufferImpl::BackendFramebufferObject, 6>
g_fboTwinLookupMemo;
// Cached addresses of the frontend's framebuffer binding slots. The frontend
// getter linear-scans its slot array per call and the draw path asks for these
// slots several times per draw (FBO sync, FBO texture-attachment sync, the
// draw-buffer broadcast memo, the draw-time bind). The slots are by-value
// members of the GLContext, so their addresses are a pure function of the
// context's address: the cache is keyed on the raw context pointer, and a
// recreated context landing on the SAME address has its slots at the same
// addresses again - the cached pointers cannot go stale. Invalidation is
// exactly the pointer compare below.
using FbBindingSlot =
std::remove_reference_t<decltype(MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
static const MG_State::GLState::GLContext* g_fbSlotCacheContext = nullptr;
static Array<FbBindingSlot*, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fbSlotCache = {};
static inline FbBindingSlot& GetFramebufferBindingSlotFast(FramebufferTarget target) {
MG_State::GLState::GLContext* ctx = MG_State::pGLContext.get();
if (ctx != g_fbSlotCacheContext) {
for (SizeT i = 0; i < g_fbSlotCache.size(); ++i) {
g_fbSlotCache[i] = &ctx->GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));
}
g_fbSlotCacheContext = ctx;
}
return *g_fbSlotCache[SizeT(target)];
}
static Bool IsDualSourceBlendFactor(BlendFactor v) {
switch (v) {
@@ -390,9 +420,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
BindBufferId(glTarget, backendResource->id);
}
// `vaoConfigVersion` is the caller's early read of currentVAOObject->GetConfigVersion():
// the VAO's config fields live on a cache line the draw path touches nowhere else, and
// cycling section VAOs makes that a guaranteed miss - reading it at the top of
// PrepareForDraw overlaps the miss with the program/texture-key work instead of
// stalling the memo check below. Nothing between the read and here can move it
// (only frontend GL entry points mutate VAO config, none run inside a preparation).
void SyncNeccessaryBuffers(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Bool includeIBO = false,
Bool includeIndirectBuffer = false) {
VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Uint32 vaoConfigVersion,
Bool includeIBO = false, Bool includeIndirectBuffer = false) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
@@ -422,7 +458,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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();
const Uint32 configVersion = vaoConfigVersion;
if (memo && memo->valid && memo->configVersion == configVersion) {
if (memo->vboCleanEpoch != bufferEpoch) {
Bool allClean = true;
@@ -1233,7 +1269,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// registry keeps a backend object alive until its frontend texture expires, which an
// attached texture cannot. A renderbuffer-only FBO - the common Minecraft frame -
// reduces to the key compare and an empty loop.
const auto& drawSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw);
const auto& drawSlot = GetFramebufferBindingSlotFast(FramebufferTarget::Draw);
const auto& currentFBO = drawSlot.GetBoundObject();
if (currentFBO) {
const Uint16 fboSlotVersion = drawSlot.GetVersion();
@@ -1341,7 +1377,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr;
for (auto& target : fboTargets) {
auto& slot = MG_State::pGLContext->GetFramebufferBindingSlot(target);
auto& slot = GetFramebufferBindingSlotFast(target);
auto& currentFBO = slot.GetBoundObject();
// The three memos together say "this target is already synced": which object is
@@ -1901,7 +1937,7 @@ 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.
{
const auto& drawSlot = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw);
const auto& drawSlot = GetFramebufferBindingSlotFast(FramebufferTarget::Draw);
const auto& drawFBO = drawSlot.GetBoundObject();
const Uint16 slotVersion = drawSlot.GetVersion();
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
@@ -1955,7 +1991,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& slot = MG_State::pGLContext->GetFramebufferBindingSlot(target);
auto& slot = GetFramebufferBindingSlotFast(target);
// No fast path on the binding slot's version. It is a 16-bit counter that only
// ForceBindCurrentFBO ever stamps here, so the comparison was against an arbitrarily old
// snapshot and any later slot version that happened to land on it - one wrap of the
@@ -1965,12 +2001,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
//
// Skipping the work is BindFramebufferId's job anyway: it shadows the driver's own
// draw/read bindings and drops the glBindFramebuffer when the target already holds the id,
// which is where the cost actually is. What is left here is one registry lookup.
// which is where the cost actually is. What is left here is one registry lookup,
// and the twin memo replaces even that with an array probe on the steady path.
const auto& currentFBO = slot.GetBoundObject();
if (currentFBO && currentFBO != MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
auto* backendFBOSlot = FramebufferImpl::g_backendFramebufferObjects.Find(currentFBO.get());
if (backendFBOSlot && *backendFBOSlot) {
(*backendFBOSlot)->Bind(target);
FramebufferImpl::BackendFramebufferObject* twin = g_fboTwinLookupMemo.Lookup(currentFBO);
if (!twin) {
auto* backendFBOSlot = FramebufferImpl::g_backendFramebufferObjects.Find(currentFBO.get());
if (backendFBOSlot && *backendFBOSlot) {
twin = backendFBOSlot->get();
g_fboTwinLookupMemo.Store(currentFBO, twin);
}
}
if (twin) {
twin->Bind(target);
} else {
MGLOG_E("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
@@ -2013,7 +2057,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& slot = MG_State::pGLContext->GetFramebufferBindingSlot(target);
auto& slot = GetFramebufferBindingSlotFast(target);
const auto& fbo = slot.GetBoundObject();
SyncAndBindFramebufferObject(fbo, target);
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
@@ -2022,7 +2066,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
static void BindCurrentProgramWithResources(
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
const TextureImpl::DrawTextureSyncKeys& keys);
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
@@ -2037,6 +2082,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
currentVAO ? VertexArrayImpl::ResolveVaoTwin(currentVAO) : nullptr;
// Early config-version read: see the note on SyncNeccessaryBuffers - issuing
// the (cold-line) load here overlaps its miss with the resolves below.
const Uint32 vaoConfigVersion = currentVAO ? currentVAO->GetConfigVersion() : 0;
// 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
@@ -2045,7 +2093,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, syncBit & DrawSyncBit::IndexBuffer,
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion,
syncBit & DrawSyncBit::IndexBuffer,
syncBit & DrawSyncBit::IndirectBuffer);
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
TextureImpl::SyncNeccessaryTextures(textureKeys);
@@ -2069,7 +2118,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, currentProgram);
BindCurrentTextures(textureKeys, currentProgram);
BindCurrentProgramWithResources(currentProgram);
BindCurrentProgramWithResources(currentProgram, textureKeys);
// Last: opening the capture span needs the program current and the capture
// buffers bound, and ES rejects most binding changes once it is open.
@@ -2234,13 +2283,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
return nullptr;
}
// Puts each touched unit's frontend sampler object on the backend unit. Deliberately
// NOT part of the memo below: BindCurrentProgramWithResources rewrites the sampler of
// every unit its program samples right after this runs (raw-depth-fetch substitution,
// per-program sampler objects), so the sampler shadow this leaves behind is not what
// the next call would find - a memo keyed on it could never hit. The step is a pointer
// compare per unit in the common case, far below the resolution cost the memo removes.
static void BindCurrentUnitSamplers(Int maxTouchedUnit) {
// Puts each touched unit's frontend sampler object on the backend unit. Not part
// of the texture memo below (BindCurrentProgramWithResources may rewrite sampled
// units' samplers right after this runs - raw-depth-fetch substitution,
// per-program sampler objects); instead the walk carries its own memo:
//
// The walk is a pure function of WHICH sampler object each touched unit holds -
// covered by (context id, unit-bindings epoch, high-water mark; the epoch
// snapshot includes each unit's sampler object and bumps when the mark moves) -
// and replaying it as "do nothing" additionally requires the sampler bindings it
// left to still be on the driver, which the row compare against
// g_boundSamplersCache (the shadow every sampler bind routes through) proves.
// Any later writer - the program pass substituting a raw-depth sampler, a
// frontend sampler bind/unbind (epoch), the ES-context generation moving - lands
// in one of those inputs. A unit whose sampler had no backend object yet leaves
// its row untouched; the object is only created by the program pass, whose Bind
// moves the row and thereby re-opens this memo, so the miss cannot be latched.
static Uint64 g_unitSamplerWalkContextId = 0;
static Uint64 g_unitSamplerWalkEpoch = 0;
static Int g_unitSamplerWalkMaxUnit = -1;
static Uint g_unitSamplerWalkContextGeneration = 0;
static Bool g_unitSamplerWalkValid = false;
static decltype(SamplerImpl::g_boundSamplersCache) g_unitSamplerWalkRows{};
static void BindCurrentUnitSamplers(const TextureImpl::DrawTextureSyncKeys& keys) {
const Int maxTouchedUnit = keys.maxTouchedUnit;
const SizeT rowBytes =
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(SamplerImpl::g_boundSamplersCache[0]);
if (g_unitSamplerWalkValid && g_unitSamplerWalkContextId == keys.contextId &&
g_unitSamplerWalkEpoch == keys.unitBindingsEpoch && g_unitSamplerWalkMaxUnit == maxTouchedUnit &&
g_unitSamplerWalkContextGeneration == TextureImpl::g_textureContextGeneration &&
std::memcmp(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes) == 0) {
return;
}
g_unitSamplerWalkValid = false;
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
if (samplerObject) {
@@ -2254,6 +2330,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
SamplerImpl::UnbindSampler(unit);
}
}
g_unitSamplerWalkContextId = keys.contextId;
g_unitSamplerWalkEpoch = keys.unitBindingsEpoch;
g_unitSamplerWalkMaxUnit = maxTouchedUnit;
g_unitSamplerWalkContextGeneration = TextureImpl::g_textureContextGeneration;
std::memcpy(g_unitSamplerWalkRows.data(), SamplerImpl::g_boundSamplersCache.data(), rowBytes);
g_unitSamplerWalkValid = true;
}
// Memo of the resolved per-unit TEXTURE bindings, so a steady-state draw loop stops
@@ -2305,7 +2387,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint contextGeneration = 0;
decltype(TextureImpl::g_boundTexturesCache) boundTextures{};
};
static ResolvedTextureBindingMemo g_resolvedTextureBindingMemo;
// Small per-PROGRAM memo set, not one global: the program is part of the key
// (it arbitrates aliased native targets), so a frame that cycles a handful of
// programs over UNCHANGED unit bindings - Sodium issues 62 glUseProgram per
// frame - invalidated a single memo on every switch and re-ran the full
// three-pass slot walk for an answer that had not moved. Entries are selected
// by raw program pointer; everything a recycled address could confuse is
// caught by the full key compare (lifetime id) exactly as before. Round-robin
// eviction; 4 entries cover the shader set a Minecraft-shaped frame cycles.
static Array<ResolvedTextureBindingMemo, 4> g_resolvedTextureBindingMemos;
static Uint32 g_resolvedTextureBindingMemoCursor = 0;
// Rebinds every frontend texture unit's textures (and sampler objects) on the
// backend context. Needed before draws AND compute dispatches: content syncs
@@ -2322,7 +2413,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Units past the frontend's high-water mark have provably-empty slots.
const Int maxTouchedUnit = keys.maxTouchedUnit;
auto& memo = g_resolvedTextureBindingMemo;
// Entry selection by program pointer; a missing program takes the round-robin
// victim. WHICH entry is used is only a performance choice - correctness sits
// entirely in the full key + shadow compare below, unchanged from the single
// memo this set replaces.
const void* programKey = static_cast<const void*>(currentProgram.get());
ResolvedTextureBindingMemo* memoSlot = nullptr;
for (auto& candidate : g_resolvedTextureBindingMemos) {
if (candidate.valid && candidate.program == programKey) {
memoSlot = &candidate;
break;
}
}
if (!memoSlot) {
g_resolvedTextureBindingMemoCursor =
(g_resolvedTextureBindingMemoCursor + 1) % g_resolvedTextureBindingMemos.size();
memoSlot = &g_resolvedTextureBindingMemos[g_resolvedTextureBindingMemoCursor];
}
auto& memo = *memoSlot;
const SizeT shadowBytes =
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
@@ -2330,7 +2438,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo.maxTouchedUnit == maxTouchedUnit &&
memo.unitBindingsEpoch == unitBindingsEpoch &&
memo.samplingResolutionGeneration == keys.samplingGeneration &&
memo.program == static_cast<const void*>(currentProgram.get()) &&
memo.program == programKey &&
memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) &&
memo.programBackendStateVersion ==
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
@@ -2346,7 +2454,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo.maxTouchedUnit = maxTouchedUnit;
memo.unitBindingsEpoch = unitBindingsEpoch;
memo.samplingResolutionGeneration = keys.samplingGeneration;
memo.program = currentProgram.get();
memo.program = programKey;
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
@@ -2356,7 +2464,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
BindCurrentUnitSamplers(maxTouchedUnit);
BindCurrentUnitSamplers(keys);
}
void BindCurrentTextures() {
@@ -2370,7 +2478,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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 SharedPtr<MG_State::GLState::ProgramObject>& currentProgram) {
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
const TextureImpl::DrawTextureSyncKeys& keys) {
if (currentProgram && currentProgram->GetLinkStatus()) {
#ifdef TRACY_ENABLE
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
@@ -2512,65 +2621,119 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Sampler unit binding: backend locations are cached at link time;
// glUniform1i is program state, so it is only re-issued when the
// frontend-assigned unit differs from what this program last saw.
for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) {
const auto unit =
currentProgram->GetUniformSamplerOrImageUnitIndex(samplerBinding.frontendLocation);
if (unit == -1) continue;
if (samplerBinding.lastAssignedUnit != unit) {
g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit);
samplerBinding.lastAssignedUnit = unit;
//
// The whole pass sits behind the per-twin SamplerPassMemo (see its
// declaration for the invalidation enumeration): while the keys hold
// and every previously touched unit's sampler-shadow row is exactly
// what this pass last left there, re-running it is a provable no-op.
auto& samplerPassMemo = backendProgram.GetSamplerPassMemo();
const Uint32 programBackendStateVersion = currentProgram->GetBackendStateVersion();
Bool samplerPassClean =
samplerPassMemo.valid && samplerPassMemo.contextId == keys.contextId &&
samplerPassMemo.unitBindingsEpoch == keys.unitBindingsEpoch &&
samplerPassMemo.samplingGeneration == keys.samplingGeneration &&
samplerPassMemo.backendStateVersion == programBackendStateVersion &&
samplerPassMemo.textureContextGeneration == TextureImpl::g_textureContextGeneration;
if (samplerPassClean) {
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
if (SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]] !=
samplerPassMemo.rows[i]) {
samplerPassClean = false;
break;
}
}
}
if (!samplerPassClean) {
constexpr SizeT kMaxMemoEntries =
PrgramImpl::BackendProgramObjectImpl::SamplerPassMemo::kMaxEntries;
samplerPassMemo.valid = false;
samplerPassMemo.count = 0;
Bool memoisable = true;
for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) {
const auto unit =
currentProgram->GetUniformSamplerOrImageUnitIndex(samplerBinding.frontendLocation);
if (unit == -1) continue;
// Record the touched unit for the memo; a pass touching more
// units than the memo can carry (or an out-of-range unit)
// simply never memoises.
if (memoisable && samplerPassMemo.count < kMaxMemoEntries && unit >= 0 &&
unit < static_cast<Int>(SamplerImpl::g_boundSamplersCache.size())) {
samplerPassMemo.units[samplerPassMemo.count++] = static_cast<Uint8>(unit);
} else {
memoisable = false;
}
if (samplerBinding.lastAssignedUnit != unit) {
g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit);
samplerBinding.lastAssignedUnit = unit;
}
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& samplerObject = textureUnit.GetSamplerObject();
const auto& texture2D = textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
auto& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
auto& samplerObject = textureUnit.GetSamplerObject();
const auto& texture2D =
textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
// ES has no per-texture/sampler LOD bias, so the transpiled ESSL folds
// it in from a uniform (PrgramImpl::EmulateTextureLodBias). A bound
// sampler object overrides the texture's own sampler state, as in GL.
if (samplerBinding.lodBiasLocation >= 0) {
Float lodBias = 0.0f;
if (samplerObject) {
lodBias = samplerObject->GetLodBias();
} else if (const auto sampledTarget =
SamplerUniformTextureTarget(samplerBinding.uniformType);
sampledTarget != TextureTarget::Unknown) {
const auto& boundTexture =
textureUnit.GetBindingSlot(sampledTarget).GetBoundObject();
if (boundTexture && boundTexture->GetSamplerObject()) {
lodBias = boundTexture->GetSamplerObject()->GetLodBias();
// ES has no per-texture/sampler LOD bias, so the transpiled ESSL folds
// it in from a uniform (PrgramImpl::EmulateTextureLodBias). A bound
// sampler object overrides the texture's own sampler state, as in GL.
if (samplerBinding.lodBiasLocation >= 0) {
Float lodBias = 0.0f;
if (samplerObject) {
lodBias = samplerObject->GetLodBias();
} else if (const auto sampledTarget =
SamplerUniformTextureTarget(samplerBinding.uniformType);
sampledTarget != TextureTarget::Unknown) {
const auto& boundTexture =
textureUnit.GetBindingSlot(sampledTarget).GetBoundObject();
if (boundTexture && boundTexture->GetSamplerObject()) {
lodBias = boundTexture->GetSamplerObject()->GetLodBias();
}
}
if (lodBias != samplerBinding.lastAssignedLodBias) {
g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias);
samplerBinding.lastAssignedLodBias = lodBias;
}
}
if (lodBias != samplerBinding.lastAssignedLodBias) {
g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias);
samplerBinding.lastAssignedLodBias = lodBias;
const SharedPtr<MG_State::GLState::SamplerObject>* rawDepthSamplerObject =
&samplerObject;
if (!*rawDepthSamplerObject && texture2D) {
rawDepthSamplerObject = &texture2D->GetSamplerObject();
}
}
const SharedPtr<MG_State::GLState::SamplerObject>* rawDepthSamplerObject = &samplerObject;
if (!*rawDepthSamplerObject && texture2D) {
rawDepthSamplerObject = &texture2D->GetSamplerObject();
}
if (samplerBinding.uniformType == GL_SAMPLER_2D && texture2D &&
NeedsRawDepthFetchSampler(*rawDepthSamplerObject, texture2D->GetFormat())) {
GetRawDepthFetchSampler()->Bind(unit);
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
} else if (samplerObject) {
auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject);
if (!backendSampler) {
auto& backendObj = SamplerImpl::g_backendSamplerObjects.GetOrCreate(samplerObject);
if (!backendObj) {
backendObj = MakeShared<SamplerImpl::BackendSamplerObject>();
if (samplerBinding.uniformType == GL_SAMPLER_2D && texture2D &&
NeedsRawDepthFetchSampler(*rawDepthSamplerObject, texture2D->GetFormat())) {
GetRawDepthFetchSampler()->Bind(unit);
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
} else if (samplerObject) {
auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject);
if (!backendSampler) {
auto& backendObj =
SamplerImpl::g_backendSamplerObjects.GetOrCreate(samplerObject);
if (!backendObj) {
backendObj = MakeShared<SamplerImpl::BackendSamplerObject>();
}
backendSampler = backendObj.get();
}
backendSampler = backendObj.get();
backendSampler->SyncToBackend(samplerObject);
// Syncing the object's parameters is not the same as putting it on the
// unit: without this the driver kept sampling with the texture's own
// parameters and every sampler object was inert.
backendSampler->Bind(unit);
} else {
SamplerImpl::UnbindSampler(unit);
}
backendSampler->SyncToBackend(samplerObject);
// Syncing the object's parameters is not the same as putting it on the
// unit: without this the driver kept sampling with the texture's own
// parameters and every sampler object was inert.
backendSampler->Bind(unit);
} else {
SamplerImpl::UnbindSampler(unit);
}
if (memoisable) {
// Snapshot the rows AFTER the pass: they are exactly what a
// clean replay must find untouched.
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
samplerPassMemo.rows[i] = SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]];
}
samplerPassMemo.contextId = keys.contextId;
samplerPassMemo.unitBindingsEpoch = keys.unitBindingsEpoch;
samplerPassMemo.samplingGeneration = keys.samplingGeneration;
samplerPassMemo.backendStateVersion = programBackendStateVersion;
samplerPassMemo.textureContextGeneration = TextureImpl::g_textureContextGeneration;
samplerPassMemo.valid = true;
}
}
}
@@ -2753,7 +2916,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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(currentProgram);
BindCurrentProgramWithResources(currentProgram, textureKeys);
}
GLuint GetBackendProgramId(GLuint program) {
+47 -5
View File
@@ -1170,13 +1170,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
return !g_uboRing.creationFailed;
}
Bool UboRingAllocate(SizeT size, SizeT& outOffset) {
if (size == 0 || !UboRingAvailable()) return false;
// Division-based rounding: the spec doesn't promise a power-of-two
namespace {
// Division-based rounding fallback: the spec doesn't promise a power-of-two
// alignment. Slot offsets stay multiples of the alignment because every
// slot size is, and wrap padding restarts at ring offset 0.
const SizeT alignedSize =
(size + g_uboRing.alignment - 1) / g_uboRing.alignment * g_uboRing.alignment;
inline SizeT UboRingAlignUp(SizeT size, SizeT alignment) {
if ((alignment & (alignment - 1)) == 0) {
return (size + alignment - 1) & ~(alignment - 1);
}
return (size + alignment - 1) / alignment * alignment;
}
Bool UboRingAllocateSlow(SizeT size, SizeT& outOffset);
} // namespace
Bool UboRingAllocate(SizeT size, SizeT& outOffset) {
if (size == 0) return false;
// Fast path: a live ring under the current context with room before both
// the wrap boundary and the in-flight tail. Touches no GL and probes no
// frame marks - the sole caller sits behind UboRingAvailable() in the
// draw preparation, so the context checks have already run this draw.
// `tail` may be stale here (marks are only retired on Present and on the
// slow path); staleness is conservative - the in-flight span reads too
// large, the check fails, and the slow path retires marks and re-tries.
auto& ring = g_uboRing;
if (ring.id != 0 && ring.contextGeneration == g_bufferContextGeneration) {
const SizeT alignedSize = UboRingAlignUp(size, ring.alignment);
// Ring sizes are kUboRingInitialBytes (a power of two) doubled some
// number of times, so the offset modulo reduces to a mask.
static_assert((kUboRingInitialBytes & (kUboRingInitialBytes - 1)) == 0,
"ring offset mask below requires power-of-two ring sizes");
const SizeT offset = static_cast<SizeT>(ring.head & (ring.size - 1));
if (offset + alignedSize <= ring.size &&
ring.head + alignedSize - ring.tail <= ring.size) {
ring.head += alignedSize;
outOffset = offset;
return true;
}
}
return UboRingAllocateSlow(size, outOffset);
}
namespace {
Bool UboRingAllocateSlow(SizeT size, SizeT& outOffset) {
if (!UboRingAvailable()) return false;
const SizeT alignedSize = UboRingAlignUp(size, g_uboRing.alignment);
if (g_uboRing.id == 0 && !CreateUboRingStorage(alignedSize)) {
return false;
}
@@ -1253,6 +1290,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
outOffset = offset;
return true;
}
} // namespace
void* UboRingMappedPtr() { return g_uboRing.mappedPtr; }
Uint UboRingBufferId() { return g_uboRing.id; }
@@ -3721,6 +3759,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
stateProgramObject->GetExternalIndex(), m_backendProgramId);
// Every link-derived cache below (incl. m_samplerUniformBindings and its
// lastAssignedUnit/lastAssignedLodBias program-state mirrors) is rebuilt;
// the sampler-pass memo keyed on them must not survive.
m_samplerPassMemo.valid = false;
m_backendProgramUsable = true;
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
+43
View File
@@ -753,6 +753,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
void InvalidatePackStateCache();
} // namespace PixelStoreImpl
namespace SamplerImpl {
class BackendSamplerObject; // for PrgramImpl's sampler-pass memo rows below
}
// Image uniforms take their unit from the layout(binding=N) qualifier baked into
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
// assigned through glUniform1i.
@@ -799,6 +803,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
Float lastAssignedLodBias = 0.0f;
};
// Memo of the whole per-draw sampler-uniform pass (glUniform1i unit
// assignments, lod-bias uniform, raw-depth-fetch substitution and the
// per-unit sampler-object binds) in BindCurrentProgramWithResources.
// The pass is a pure function of the keys below, and its only driver-side
// effect is the sampler binding of each sampled unit, so replaying it as
// "do nothing" additionally requires those bindings to still be on the
// driver - the per-entry row compare against g_boundSamplersCache (the
// shadow every sampler bind in this backend already routes through).
//
// Invalidation enumeration:
// * sampler-uniform unit assignment (glUniform1i) and uniform-block
// binding edits -> frontend backendStateVersion;
// * any texture/sampler bind moving on any unit (incl. the high-water
// mark moving) -> unitBindingsEpoch;
// * any sampler parameter (incl. lod bias, compare mode) or texture
// shape/format change -> samplingGeneration;
// * another frontend context -> contextId (never-reused id);
// * ES context recreation -> textureContextGeneration;
// * relink / backend program rebuild -> SyncToBackend resets `valid`
// (it rebuilds m_samplerUniformBindings, whose lastAssignedUnit /
// lastAssignedLodBias dedup state this memo leans on);
// * any other writer moving a sampled unit's sampler binding
// (BindCurrentUnitSamplers on a unit-sampler change, scratch binds)
// -> the row snapshot compare.
struct SamplerPassMemo {
static constexpr SizeT kMaxEntries = 16;
Bool valid = false;
Uint8 count = 0;
Uint64 contextId = 0;
Uint64 unitBindingsEpoch = 0;
Uint64 samplingGeneration = 0;
Uint32 backendStateVersion = 0;
Uint textureContextGeneration = 0;
Array<Uint8, kMaxEntries> units{};
Array<SamplerImpl::BackendSamplerObject*, kMaxEntries> rows{};
};
BackendProgramObjectImpl();
~BackendProgramObjectImpl();
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
@@ -827,6 +868,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// reflected size when the transpiled block pads differently).
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
SamplerPassMemo& GetSamplerPassMemo() { return m_samplerPassMemo; }
// Frontend link version this backend program (and its resource caches) was
// built from; a mismatch means every link-derived cache here is stale.
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
@@ -855,6 +897,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
Uint32 m_syncedLinkVersion = ~0u;
SamplerPassMemo m_samplerPassMemo;
};
extern Uint32 g_snormFallbackClampOutputMask;