mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-10 13:18:31 +09:00
[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:
@@ -119,6 +119,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_vaoTwinLookupMemo;
|
g_vaoTwinLookupMemo;
|
||||||
static TwinLookupMemo<MG_State::GLState::ProgramObject, PrgramImpl::BackendProgramObjectImpl, 8>
|
static TwinLookupMemo<MG_State::GLState::ProgramObject, PrgramImpl::BackendProgramObjectImpl, 8>
|
||||||
g_programTwinLookupMemo;
|
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) {
|
static Bool IsDualSourceBlendFactor(BlendFactor v) {
|
||||||
switch (v) {
|
switch (v) {
|
||||||
@@ -390,9 +420,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
BindBufferId(glTarget, backendResource->id);
|
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,
|
void SyncNeccessaryBuffers(const SharedPtr<MG_State::GLState::VertexArrayObject>& currentVAOObject,
|
||||||
VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Bool includeIBO = false,
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin, Uint32 vaoConfigVersion,
|
||||||
Bool includeIndirectBuffer = false) {
|
Bool includeIBO = false, Bool includeIndirectBuffer = false) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -422,7 +458,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// read, makes the stamp stale, and re-runs the probes next draw.
|
// read, makes the stamp stale, and re-runs the probes next draw.
|
||||||
const Uint64 bufferEpoch = CurrentBufferMutationEpoch();
|
const Uint64 bufferEpoch = CurrentBufferMutationEpoch();
|
||||||
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
|
auto* memo = vaoTwin ? &vaoTwin->GetResolvedDrawBuffersMemo() : nullptr;
|
||||||
const Uint32 configVersion = currentVAOObject->GetConfigVersion();
|
const Uint32 configVersion = vaoConfigVersion;
|
||||||
if (memo && memo->valid && memo->configVersion == configVersion) {
|
if (memo && memo->valid && memo->configVersion == configVersion) {
|
||||||
if (memo->vboCleanEpoch != bufferEpoch) {
|
if (memo->vboCleanEpoch != bufferEpoch) {
|
||||||
Bool allClean = true;
|
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
|
// registry keeps a backend object alive until its frontend texture expires, which an
|
||||||
// attached texture cannot. A renderbuffer-only FBO - the common Minecraft frame -
|
// attached texture cannot. A renderbuffer-only FBO - the common Minecraft frame -
|
||||||
// reduces to the key compare and an empty loop.
|
// 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();
|
const auto& currentFBO = drawSlot.GetBoundObject();
|
||||||
if (currentFBO) {
|
if (currentFBO) {
|
||||||
const Uint16 fboSlotVersion = drawSlot.GetVersion();
|
const Uint16 fboSlotVersion = drawSlot.GetVersion();
|
||||||
@@ -1341,7 +1377,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr;
|
MG_State::GLState::FramebufferObject* lastUpdatedFBO = nullptr;
|
||||||
|
|
||||||
for (auto& target : fboTargets) {
|
for (auto& target : fboTargets) {
|
||||||
auto& slot = MG_State::pGLContext->GetFramebufferBindingSlot(target);
|
auto& slot = GetFramebufferBindingSlotFast(target);
|
||||||
auto& currentFBO = slot.GetBoundObject();
|
auto& currentFBO = slot.GetBoundObject();
|
||||||
|
|
||||||
// The three memos together say "this target is already synced": which object is
|
// 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
|
// 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.
|
// 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 auto& drawFBO = drawSlot.GetBoundObject();
|
||||||
const Uint16 slotVersion = drawSlot.GetVersion();
|
const Uint16 slotVersion = drawSlot.GetVersion();
|
||||||
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
|
const Uint16 objectVersion = drawFBO ? drawFBO->GetObjectVersion() : 0;
|
||||||
@@ -1955,7 +1991,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#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
|
// 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
|
// 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
|
// 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
|
// 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,
|
// 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();
|
const auto& currentFBO = slot.GetBoundObject();
|
||||||
if (currentFBO && currentFBO != MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
|
if (currentFBO && currentFBO != MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO) {
|
||||||
auto* backendFBOSlot = FramebufferImpl::g_backendFramebufferObjects.Find(currentFBO.get());
|
FramebufferImpl::BackendFramebufferObject* twin = g_fboTwinLookupMemo.Lookup(currentFBO);
|
||||||
if (backendFBOSlot && *backendFBOSlot) {
|
if (!twin) {
|
||||||
(*backendFBOSlot)->Bind(target);
|
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 {
|
} else {
|
||||||
MGLOG_E("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
|
MGLOG_E("No backend FBO found (maybe not synced) for current %s FBO, cannot bind FBO.",
|
||||||
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
|
(target == FramebufferTarget::Read ? "READ" : "DRAW"));
|
||||||
@@ -2013,7 +2057,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
auto& slot = MG_State::pGLContext->GetFramebufferBindingSlot(target);
|
auto& slot = GetFramebufferBindingSlotFast(target);
|
||||||
const auto& fbo = slot.GetBoundObject();
|
const auto& fbo = slot.GetBoundObject();
|
||||||
SyncAndBindFramebufferObject(fbo, target);
|
SyncAndBindFramebufferObject(fbo, target);
|
||||||
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
|
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
|
||||||
@@ -2022,7 +2066,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void BindCurrentProgramWithResources(
|
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,
|
static void BindCurrentTextures(const TextureImpl::DrawTextureSyncKeys& keys,
|
||||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram);
|
||||||
|
|
||||||
@@ -2037,6 +2082,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
const auto& currentVAO = MG_State::pGLContext->GetBoundVertexArray();
|
||||||
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
|
VertexArrayImpl::BackendVertexArrayObject* vaoTwin =
|
||||||
currentVAO ? VertexArrayImpl::ResolveVaoTwin(currentVAO) : nullptr;
|
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
|
// 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
|
// the same reason the twin resolve does: only frontend GL entry points move
|
||||||
// either, and none can run inside this preparation. GetProgramForDraw is a
|
// 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 auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys();
|
||||||
|
|
||||||
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, syncBit & DrawSyncBit::IndexBuffer,
|
BufferImpl::SyncNeccessaryBuffers(currentVAO, vaoTwin, vaoConfigVersion,
|
||||||
|
syncBit & DrawSyncBit::IndexBuffer,
|
||||||
syncBit & DrawSyncBit::IndirectBuffer);
|
syncBit & DrawSyncBit::IndirectBuffer);
|
||||||
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
|
VertexArrayImpl::SyncCurrentVAO(currentVAO, vaoTwin);
|
||||||
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
TextureImpl::SyncNeccessaryTextures(textureKeys);
|
||||||
@@ -2069,7 +2118,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, currentProgram);
|
VertexArrayImpl::SyncCurrentVertexAttributeValues(vaoTwin, currentProgram);
|
||||||
|
|
||||||
BindCurrentTextures(textureKeys, currentProgram);
|
BindCurrentTextures(textureKeys, currentProgram);
|
||||||
BindCurrentProgramWithResources(currentProgram);
|
BindCurrentProgramWithResources(currentProgram, textureKeys);
|
||||||
|
|
||||||
// Last: opening the capture span needs the program current and the capture
|
// Last: opening the capture span needs the program current and the capture
|
||||||
// buffers bound, and ES rejects most binding changes once it is open.
|
// buffers bound, and ES rejects most binding changes once it is open.
|
||||||
@@ -2234,13 +2283,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Puts each touched unit's frontend sampler object on the backend unit. Deliberately
|
// Puts each touched unit's frontend sampler object on the backend unit. Not part
|
||||||
// NOT part of the memo below: BindCurrentProgramWithResources rewrites the sampler of
|
// of the texture memo below (BindCurrentProgramWithResources may rewrite sampled
|
||||||
// every unit its program samples right after this runs (raw-depth-fetch substitution,
|
// units' samplers right after this runs - raw-depth-fetch substitution,
|
||||||
// per-program sampler objects), so the sampler shadow this leaves behind is not what
|
// per-program sampler objects); instead the walk carries its own memo:
|
||||||
// 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.
|
// The walk is a pure function of WHICH sampler object each touched unit holds -
|
||||||
static void BindCurrentUnitSamplers(Int maxTouchedUnit) {
|
// 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) {
|
for (Int unit = 0; unit <= maxTouchedUnit; ++unit) {
|
||||||
const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
const auto& samplerObject = MG_State::pGLContext->GetTextureUnitObject(unit).GetSamplerObject();
|
||||||
if (samplerObject) {
|
if (samplerObject) {
|
||||||
@@ -2254,6 +2330,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
SamplerImpl::UnbindSampler(unit);
|
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
|
// 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;
|
Uint contextGeneration = 0;
|
||||||
decltype(TextureImpl::g_boundTexturesCache) boundTextures{};
|
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
|
// Rebinds every frontend texture unit's textures (and sampler objects) on the
|
||||||
// backend context. Needed before draws AND compute dispatches: content syncs
|
// 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.
|
// Units past the frontend's high-water mark have provably-empty slots.
|
||||||
const Int maxTouchedUnit = keys.maxTouchedUnit;
|
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 =
|
const SizeT shadowBytes =
|
||||||
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
|
static_cast<SizeT>(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]);
|
||||||
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
|
const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch;
|
||||||
@@ -2330,7 +2438,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
memo.maxTouchedUnit == maxTouchedUnit &&
|
memo.maxTouchedUnit == maxTouchedUnit &&
|
||||||
memo.unitBindingsEpoch == unitBindingsEpoch &&
|
memo.unitBindingsEpoch == unitBindingsEpoch &&
|
||||||
memo.samplingResolutionGeneration == keys.samplingGeneration &&
|
memo.samplingResolutionGeneration == keys.samplingGeneration &&
|
||||||
memo.program == static_cast<const void*>(currentProgram.get()) &&
|
memo.program == programKey &&
|
||||||
memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) &&
|
memo.programLifetimeId == (currentProgram ? currentProgram->GetLifetimeId() : 0) &&
|
||||||
memo.programBackendStateVersion ==
|
memo.programBackendStateVersion ==
|
||||||
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
(currentProgram ? currentProgram->GetBackendStateVersion() : 0) &&
|
||||||
@@ -2346,7 +2454,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
memo.maxTouchedUnit = maxTouchedUnit;
|
memo.maxTouchedUnit = maxTouchedUnit;
|
||||||
memo.unitBindingsEpoch = unitBindingsEpoch;
|
memo.unitBindingsEpoch = unitBindingsEpoch;
|
||||||
memo.samplingResolutionGeneration = keys.samplingGeneration;
|
memo.samplingResolutionGeneration = keys.samplingGeneration;
|
||||||
memo.program = currentProgram.get();
|
memo.program = programKey;
|
||||||
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
memo.programLifetimeId = currentProgram ? currentProgram->GetLifetimeId() : 0;
|
||||||
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
|
memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0;
|
||||||
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
|
memo.programLinked = currentProgram && currentProgram->GetLinkStatus();
|
||||||
@@ -2356,7 +2464,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BindCurrentUnitSamplers(maxTouchedUnit);
|
BindCurrentUnitSamplers(keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BindCurrentTextures() {
|
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
|
// this as much as draws do — e.g. Flywheel's cull shader reads the
|
||||||
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
|
// _FlwFrameUniforms block and the _flw_depthPyramid sampler.
|
||||||
static void BindCurrentProgramWithResources(
|
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()) {
|
if (currentProgram && currentProgram->GetLinkStatus()) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
||||||
@@ -2512,65 +2621,119 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// Sampler unit binding: backend locations are cached at link time;
|
// Sampler unit binding: backend locations are cached at link time;
|
||||||
// glUniform1i is program state, so it is only re-issued when the
|
// glUniform1i is program state, so it is only re-issued when the
|
||||||
// frontend-assigned unit differs from what this program last saw.
|
// frontend-assigned unit differs from what this program last saw.
|
||||||
for (auto& samplerBinding : backendProgram.GetSamplerUniformBindings()) {
|
//
|
||||||
const auto unit =
|
// The whole pass sits behind the per-twin SamplerPassMemo (see its
|
||||||
currentProgram->GetUniformSamplerOrImageUnitIndex(samplerBinding.frontendLocation);
|
// declaration for the invalidation enumeration): while the keys hold
|
||||||
if (unit == -1) continue;
|
// and every previously touched unit's sampler-shadow row is exactly
|
||||||
if (samplerBinding.lastAssignedUnit != unit) {
|
// what this pass last left there, re-running it is a provable no-op.
|
||||||
g_GLESFuncs.glUniform1i(samplerBinding.backendLocation, unit);
|
auto& samplerPassMemo = backendProgram.GetSamplerPassMemo();
|
||||||
samplerBinding.lastAssignedUnit = unit;
|
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& textureUnit = MG_State::pGLContext->GetTextureUnitObject(unit);
|
||||||
auto& samplerObject = textureUnit.GetSamplerObject();
|
auto& samplerObject = textureUnit.GetSamplerObject();
|
||||||
const auto& texture2D = textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
const auto& texture2D =
|
||||||
|
textureUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
|
||||||
|
|
||||||
// ES has no per-texture/sampler LOD bias, so the transpiled ESSL folds
|
// ES has no per-texture/sampler LOD bias, so the transpiled ESSL folds
|
||||||
// it in from a uniform (PrgramImpl::EmulateTextureLodBias). A bound
|
// it in from a uniform (PrgramImpl::EmulateTextureLodBias). A bound
|
||||||
// sampler object overrides the texture's own sampler state, as in GL.
|
// sampler object overrides the texture's own sampler state, as in GL.
|
||||||
if (samplerBinding.lodBiasLocation >= 0) {
|
if (samplerBinding.lodBiasLocation >= 0) {
|
||||||
Float lodBias = 0.0f;
|
Float lodBias = 0.0f;
|
||||||
if (samplerObject) {
|
if (samplerObject) {
|
||||||
lodBias = samplerObject->GetLodBias();
|
lodBias = samplerObject->GetLodBias();
|
||||||
} else if (const auto sampledTarget =
|
} else if (const auto sampledTarget =
|
||||||
SamplerUniformTextureTarget(samplerBinding.uniformType);
|
SamplerUniformTextureTarget(samplerBinding.uniformType);
|
||||||
sampledTarget != TextureTarget::Unknown) {
|
sampledTarget != TextureTarget::Unknown) {
|
||||||
const auto& boundTexture =
|
const auto& boundTexture =
|
||||||
textureUnit.GetBindingSlot(sampledTarget).GetBoundObject();
|
textureUnit.GetBindingSlot(sampledTarget).GetBoundObject();
|
||||||
if (boundTexture && boundTexture->GetSamplerObject()) {
|
if (boundTexture && boundTexture->GetSamplerObject()) {
|
||||||
lodBias = boundTexture->GetSamplerObject()->GetLodBias();
|
lodBias = boundTexture->GetSamplerObject()->GetLodBias();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lodBias != samplerBinding.lastAssignedLodBias) {
|
||||||
|
g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias);
|
||||||
|
samplerBinding.lastAssignedLodBias = lodBias;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lodBias != samplerBinding.lastAssignedLodBias) {
|
const SharedPtr<MG_State::GLState::SamplerObject>* rawDepthSamplerObject =
|
||||||
g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias);
|
&samplerObject;
|
||||||
samplerBinding.lastAssignedLodBias = lodBias;
|
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 &&
|
if (samplerBinding.uniformType == GL_SAMPLER_2D && texture2D &&
|
||||||
NeedsRawDepthFetchSampler(*rawDepthSamplerObject, texture2D->GetFormat())) {
|
NeedsRawDepthFetchSampler(*rawDepthSamplerObject, texture2D->GetFormat())) {
|
||||||
GetRawDepthFetchSampler()->Bind(unit);
|
GetRawDepthFetchSampler()->Bind(unit);
|
||||||
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
|
MGLOG_D("Using raw depth fetch sampler on unit %d.", unit);
|
||||||
} else if (samplerObject) {
|
} else if (samplerObject) {
|
||||||
auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject);
|
auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject);
|
||||||
if (!backendSampler) {
|
if (!backendSampler) {
|
||||||
auto& backendObj = SamplerImpl::g_backendSamplerObjects.GetOrCreate(samplerObject);
|
auto& backendObj =
|
||||||
if (!backendObj) {
|
SamplerImpl::g_backendSamplerObjects.GetOrCreate(samplerObject);
|
||||||
backendObj = MakeShared<SamplerImpl::BackendSamplerObject>();
|
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
|
if (memoisable) {
|
||||||
// unit: without this the driver kept sampling with the texture's own
|
// Snapshot the rows AFTER the pass: they are exactly what a
|
||||||
// parameters and every sampler object was inert.
|
// clean replay must find untouched.
|
||||||
backendSampler->Bind(unit);
|
for (Uint i = 0; i < samplerPassMemo.count; ++i) {
|
||||||
} else {
|
samplerPassMemo.rows[i] = SamplerImpl::g_boundSamplersCache[samplerPassMemo.units[i]];
|
||||||
SamplerImpl::UnbindSampler(unit);
|
}
|
||||||
|
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:
|
// Compute programs need the same per-program resource sync as draws:
|
||||||
// uniform-block bindings and sampler units only exist through the API
|
// uniform-block bindings and sampler units only exist through the API
|
||||||
// because layout(binding) is stripped from the transpiled ESSL.
|
// because layout(binding) is stripped from the transpiled ESSL.
|
||||||
BindCurrentProgramWithResources(currentProgram);
|
BindCurrentProgramWithResources(currentProgram, textureKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint GetBackendProgramId(GLuint program) {
|
GLuint GetBackendProgramId(GLuint program) {
|
||||||
|
|||||||
@@ -1170,13 +1170,50 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return !g_uboRing.creationFailed;
|
return !g_uboRing.creationFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool UboRingAllocate(SizeT size, SizeT& outOffset) {
|
namespace {
|
||||||
if (size == 0 || !UboRingAvailable()) return false;
|
// Division-based rounding fallback: the spec doesn't promise a power-of-two
|
||||||
// Division-based rounding: the spec doesn't promise a power-of-two
|
|
||||||
// alignment. Slot offsets stay multiples of the alignment because every
|
// alignment. Slot offsets stay multiples of the alignment because every
|
||||||
// slot size is, and wrap padding restarts at ring offset 0.
|
// slot size is, and wrap padding restarts at ring offset 0.
|
||||||
const SizeT alignedSize =
|
inline SizeT UboRingAlignUp(SizeT size, SizeT alignment) {
|
||||||
(size + g_uboRing.alignment - 1) / g_uboRing.alignment * g_uboRing.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)) {
|
if (g_uboRing.id == 0 && !CreateUboRingStorage(alignedSize)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1253,6 +1290,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
outOffset = offset;
|
outOffset = offset;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void* UboRingMappedPtr() { return g_uboRing.mappedPtr; }
|
void* UboRingMappedPtr() { return g_uboRing.mappedPtr; }
|
||||||
Uint UboRingBufferId() { return g_uboRing.id; }
|
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",
|
MGLOG_D("Syncing program to backend. State program ID: %u, Backend ID: %u",
|
||||||
stateProgramObject->GetExternalIndex(), m_backendProgramId);
|
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_backendProgramUsable = true;
|
||||||
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
m_snormFallbackClampOutputMask = g_snormFallbackClampOutputMask;
|
||||||
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
m_unormFallbackClampOutputMask = g_unormFallbackClampOutputMask;
|
||||||
|
|||||||
@@ -753,6 +753,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
void InvalidatePackStateCache();
|
void InvalidatePackStateCache();
|
||||||
} // namespace PixelStoreImpl
|
} // 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
|
// 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
|
// the transpiled ESSL; unlike samplers they must not (and in ES cannot) be
|
||||||
// assigned through glUniform1i.
|
// assigned through glUniform1i.
|
||||||
@@ -799,6 +803,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Float lastAssignedLodBias = 0.0f;
|
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();
|
||||||
~BackendProgramObjectImpl();
|
~BackendProgramObjectImpl();
|
||||||
void SyncToBackend(const SharedPtr<MG_State::GLState::ProgramObject>& stateProgramObject);
|
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).
|
// reflected size when the transpiled block pads differently).
|
||||||
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
Int GetGlobalUboBackendBlockSize() const { return m_globalUboBackendBlockSize; }
|
||||||
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
BufferImpl::UboRingAllocation& GetGlobalUboRingAllocation() { return m_globalUboRingAllocation; }
|
||||||
|
SamplerPassMemo& GetSamplerPassMemo() { return m_samplerPassMemo; }
|
||||||
// Frontend link version this backend program (and its resource caches) was
|
// Frontend link version this backend program (and its resource caches) was
|
||||||
// built from; a mismatch means every link-derived cache here is stale.
|
// built from; a mismatch means every link-derived cache here is stale.
|
||||||
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
Uint32 GetSyncedLinkVersion() const { return m_syncedLinkVersion; }
|
||||||
@@ -855,6 +897,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
Uint32 m_lastUploadedGlobalUboVersion = ~0u;
|
||||||
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
BufferImpl::UboRingAllocation m_globalUboRingAllocation;
|
||||||
Uint32 m_syncedLinkVersion = ~0u;
|
Uint32 m_syncedLinkVersion = ~0u;
|
||||||
|
SamplerPassMemo m_samplerPassMemo;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Uint32 g_snormFallbackClampOutputMask;
|
extern Uint32 g_snormFallbackClampOutputMask;
|
||||||
|
|||||||
Reference in New Issue
Block a user