[Refactor] (Espryt): drive the per-draw texture sync from the pushed sampler-view set and give a read-only attachment its texture parameters

This commit is contained in:
2026-09-08 18:44:25 -04:00
committed by rereview
parent 31e6a7d6d8
commit 0b12f89f3b
+265 -4
View File
@@ -1804,7 +1804,61 @@ namespace MobileGL::MG_Backend::DirectGLES {
// a context switch, a high-water-mark move or a bindings change each recapture and
// bump - so epoch equality alone proves the bindings a consumer resolved against
// are the bindings on the units now.
#if MOBILEGL_PIPE_PUSH
// P4a e2 (D-G1). THE UNIT-BINDINGS EPOCH DERIVATION, REPLACED BY THE RECORDS' SERIALS.
//
// CurrentUnitBindingsEpoch below exists to answer one question - "did WHAT the touched
// units hold change?" - without re-deriving on a redundant re-bind, and it pays for the
// answer with a per-unit snapshot walk over every binding slot plus the unit's sampler
// object, run whenever the frontend's texture bind generation moves (26.2 moves it
// around every texture-unit switch). The applier already holds the answer:
// set_sampler_views and bind_sampler_states go out through a ContentHash suppressor,
// so their serials move IF AND ONLY IF the resolved per-unit view set or the per-unit
// sampler set really moved. Two Uint64 reads replace the walk, and the suppressor is
// what makes that sound - the hash is over every entry of each tail, so suppression
// cannot hide a change.
//
// BOTH serials, because the snapshot covered both halves: slotObjects is the view set
// and samplerObject is the sampler-state set.
//
// DECLINES until both windows have arrived at least once, and the COUNT is what says
// so rather than the serial: MGPipeApplierReset advances every P4a working-state
// serial whether or not anything was ever emitted, so a serial is never "no set has
// been received". A declined answer leaves the caller on the snapshot walk, which is
// the honest thing for a tree whose client half has not landed.
//
// The two arms' epoch values share one number space during the transition (a process
// can run the walk before the first emission and the serials after it). That is safe
// by construction: consumers only ever ask "is this the same value I stamped", the
// walk's counter is small and dense and this one is a 64-bit mix, so the two cannot
// meet except with vanishing probability - and a collision costs a spare rebuild.
static Bool UnitBindingsEpochFromRecords(Uint64& out) {
const auto& st = MG_Pipe::MGPipeApplier();
if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return false;
// Local, because the tracker's MGPipeMixShutter lives in MG_Impl and no backend
// translation unit may reach for it.
const Uint64 mixed =
(st.SamplerViewsSerial * 0x9E3779B97F4A7C15ull) ^ (st.SamplerStatesSerial + 0x165667B19E3779F9ull);
out = mixed;
return true;
}
#endif
static Uint64 CurrentUnitBindingsEpoch(Int maxTouchedUnit) {
#if MOBILEGL_PIPE_PUSH
if (EsprytDrawSamplerHandlesEnabled()) {
Uint64 epochFromRecords = 0;
if (UnitBindingsEpochFromRecords(epochFromRecords)) {
// No accessor reads and no walk on this arm; the two PipeStats accessor
// ticks below are not counted because no accessor was called.
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytUnitBindingsEpoch,
/*hit=*/true);
}
return epochFromRecords;
}
}
#endif
const Uint64 contextId = MGB_CTX->GetTextureContextId();
const Uint64 bindGeneration = MGB_CTX->GetTextureBindGeneration();
if (MG_Util::PipeStats::Enabled()) {
@@ -1889,6 +1943,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Uint16 g_fboTextureSyncListObjectVersion = 0;
static Uint64 g_fboTextureSyncListContextId = 0;
static Uint g_fboTextureSyncListContextGeneration = 0;
#if MOBILEGL_PIPE_PUSH
// P4a e2. The record-keyed half of the same memo. The draw framebuffer record's
// ContentHash replaces the two VERSION halves of the key - the hash is taken over
// every field the record carries, so an attachment edit always moves it, and Fbo is
// inside it, so a recycled framebuffer handle cannot be suppressed against its
// predecessor. THE IDENTITY HALF IS STILL A POINTER COMPARE and deliberately stays
// one: this list borrows the attachment's own SharedPtr slots, so what it needs to
// know is that the frontend object it borrowed from is the one bound now, and that
// question has no handle in it until package D re-keys the twin tables. Held in its
// own fields with its own flag rather than reusing the versions', because a process
// runs the frontend key before the client's first emission and the record key after
// it, and one field carrying two key shapes is how a stale half gets compared against
// a live one.
static Uint64 g_fboTextureSyncListContentHash = 0;
static Bool g_fboTextureSyncListRecordKeyed = false;
#endif
// The frontend texture-state keys the per-draw texture stages
// (SyncNeccessaryTextures, then BindCurrentTextures) both consume. Captured
@@ -1908,16 +1978,129 @@ namespace MobileGL::MG_Backend::DirectGLES {
DrawTextureSyncKeys keys;
keys.contextId = MGB_CTX->GetTextureContextId();
// Units past the frontend's high-water mark have provably-empty slots.
//
// P4a e2 DELIBERATELY DOES NOT take this off the sampler-view window's Count, and
// the reason is asymmetric risk. D-G2 makes Count the client's spelling of exactly
// this number, so the substitution looks free - but the two failure directions are
// not alike: a window that is larger than the frontend's mark costs a walk over
// provably-empty units, while one that is SMALLER silently drops the sync of a
// texture a draw is about to sample, which no gate on this tree can see because
// this tree emits no windows at all. The design assigns the high-water mark to the
// CLIENT as the count argument and says nothing about re-deriving it server-side.
// It is one accessor read per draw; the integrator can move it in one line once a
// tree exists where the two can be compared.
keys.maxTouchedUnit = MGB_CTX->GetMaxTouchedTextureUnit();
keys.samplingGeneration = MGB_CTX->GetSamplingResolutionGeneration();
keys.unitBindingsEpoch = CurrentUnitBindingsEpoch(keys.maxTouchedUnit);
if (MG_Util::PipeStats::Enabled()) {
// The three reads above; CurrentUnitBindingsEpoch counts its own two.
// The three reads above; CurrentUnitBindingsEpoch counts its own two when it
// takes the walk arm and none when the records answered.
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 3);
}
return keys;
}
#if MOBILEGL_PIPE_PUSH
// P4a e2 (D-E3). The READ framebuffer's texture attachments, and why this exists.
//
// A texture that is an attachment of the READ framebuffer ONLY reaches
// SyncAttachmentObject, which calls SyncMipmapsToBackend AND NOTHING ELSE - so its
// swizzle, its GL_DEPTH_STENCIL_TEXTURE_MODE, its LOD clamp and its border colour
// never reach the driver. The list that pushes texture PARAMETERS for an attachment is
// the one below, and it reads the DRAW binding slot alone. That is the gap
// ARCHITECTURE.md's D10 names, and the roadmap makes closing it a mandatory,
// red-before deliverable of this phase: set_texture_params is addressed by RESOURCE
// and is independent of any binding, so a texture's parameters exist the moment they
// move and an attachment - draw or read - gets them.
//
// DELIBERATELY NOT GATED ON THE FRAMEBUFFER SUBSYSTEM BIT. It is a deliverable of the
// phase rather than an arm of it, so both arms of the object A/B carry it and the A/B
// keeps measuring the arm rather than the arm plus a behaviour change. It is inside
// MOBILEGL_PIPE_PUSH, so the pull build is untouched.
//
// Its own list and its own key rather than an extension of the draw list's, because
// the draw list is keyed on the draw binding and its pull-build copy must stay
// textually what it was.
static Vector<UnitTextureSyncEntry> g_readFboTextureSyncList;
static MG_State::GLState::FramebufferObject* g_readFboTextureSyncListFbo = nullptr;
static Uint16 g_readFboTextureSyncListSlotVersion = 0;
static Uint16 g_readFboTextureSyncListObjectVersion = 0;
static Uint64 g_readFboTextureSyncListContextId = 0;
static Uint g_readFboTextureSyncListContextGeneration = 0;
static Uint64 g_readFboTextureSyncListContentHash = 0;
static Bool g_readFboTextureSyncListRecordKeyed = false;
static void SyncReadFramebufferTextureAttachments(const DrawTextureSyncKeys& keys,
const MG_State::GLState::FramebufferObject* drawFbo) {
const auto& readSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Read);
const auto& readFBO = readSlot.GetBoundObject();
// One object bound to both bindings has already been walked as the draw
// framebuffer, and its attachments are already in the draw list; a pointer compare
// is the whole dedupe, because a framebuffer has two or three attachments and any
// set would cost more than the syncs it saves.
if (!readFBO || readFBO.get() == drawFbo) {
g_readFboTextureSyncListFbo = nullptr;
g_readFboTextureSyncList.clear();
return;
}
const Uint16 slotVersion = readSlot.GetVersion();
const Uint16 objectVersion = readFBO->GetObjectVersion();
Bool recordKeyed = false;
Uint64 contentHash = 0;
if (EsprytDrawFramebufferHandlesEnabled()) {
const auto& record = MG_Pipe::MGPipeApplier().ReadFramebuffer;
if (!MG_Pipe::MGPipeHandleIsNull(record.Fbo)) {
recordKeyed = true;
contentHash = record.ContentHash;
}
}
const Bool listValid =
g_readFboTextureSyncListFbo == readFBO.get() &&
g_readFboTextureSyncListRecordKeyed == recordKeyed &&
(recordKeyed ? g_readFboTextureSyncListContentHash == contentHash
: (g_readFboTextureSyncListSlotVersion == slotVersion &&
g_readFboTextureSyncListObjectVersion == objectVersion)) &&
g_readFboTextureSyncListContextId == keys.contextId &&
g_readFboTextureSyncListContextGeneration == g_backendContextGeneration &&
PairingsIntact(g_readFboTextureSyncList);
if (listValid) {
for (const auto& entry : g_readFboTextureSyncList) {
// The same aggregate gate the two lists above use.
if (entry.backend->IsDrawSyncClean(entry.slot->get(), keys.contextId,
keys.samplingGeneration)) {
continue;
}
entry.backend->SyncTextureParamsToBackend(*entry.slot);
entry.backend->SyncBuiltinSamplerToBackend(*entry.slot);
entry.backend->SyncMipmapsToBackend(*entry.slot);
}
return;
}
g_readFboTextureSyncListFbo = nullptr;
g_readFboTextureSyncList.clear();
for (const auto& attachment : readFBO->GetAllAttachmentObjects()) {
if (!attachment.IsTexture()) continue;
auto& textureObject = attachment.GetTexture();
if (textureObject) {
g_readFboTextureSyncList.push_back({&textureObject, textureObject.get(),
SyncTextureObjectToBackend(textureObject).get()});
}
}
g_readFboTextureSyncListFbo = readFBO.get();
g_readFboTextureSyncListSlotVersion = slotVersion;
g_readFboTextureSyncListObjectVersion = objectVersion;
g_readFboTextureSyncListContextId = keys.contextId;
g_readFboTextureSyncListContextGeneration = g_backendContextGeneration;
g_readFboTextureSyncListContentHash = contentHash;
g_readFboTextureSyncListRecordKeyed = recordKeyed;
}
#endif // MOBILEGL_PIPE_PUSH
void SyncNeccessaryTextures(const DrawTextureSyncKeys& keys) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
@@ -2007,10 +2190,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (currentFBO) {
const Uint16 fboSlotVersion = drawSlot.GetVersion();
const Uint16 fboObjectVersion = currentFBO->GetObjectVersion();
#if MOBILEGL_PIPE_PUSH
Bool fboRecordKeyed = false;
Uint64 fboContentHash = 0;
if (EsprytDrawFramebufferHandlesEnabled()) {
const auto& drawRecord = MG_Pipe::MGPipeApplier().DrawFramebuffer;
if (!MG_Pipe::MGPipeHandleIsNull(drawRecord.Fbo)) {
fboRecordKeyed = true;
fboContentHash = drawRecord.ContentHash;
}
}
#endif
const Bool fboListValid =
g_fboTextureSyncListFbo == currentFBO.get() &&
#if MOBILEGL_PIPE_PUSH
g_fboTextureSyncListRecordKeyed == fboRecordKeyed &&
(fboRecordKeyed
? g_fboTextureSyncListContentHash == fboContentHash
: (g_fboTextureSyncListSlotVersion == fboSlotVersion &&
g_fboTextureSyncListObjectVersion == fboObjectVersion)) &&
#else
g_fboTextureSyncListSlotVersion == fboSlotVersion &&
g_fboTextureSyncListObjectVersion == fboObjectVersion &&
#endif
g_fboTextureSyncListContextId == keys.contextId &&
g_fboTextureSyncListContextGeneration == g_backendContextGeneration &&
PairingsIntact(g_fboTextureSyncList);
@@ -2041,11 +2243,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
g_fboTextureSyncListObjectVersion = fboObjectVersion;
g_fboTextureSyncListContextId = keys.contextId;
g_fboTextureSyncListContextGeneration = g_backendContextGeneration;
#if MOBILEGL_PIPE_PUSH
g_fboTextureSyncListContentHash = fboContentHash;
g_fboTextureSyncListRecordKeyed = fboRecordKeyed;
#endif
}
} else {
g_fboTextureSyncListFbo = nullptr;
g_fboTextureSyncList.clear();
}
#if MOBILEGL_PIPE_PUSH
// D-E3: and then the READ framebuffer's, which nothing else in this backend gives
// texture parameters to.
SyncReadFramebufferTextureAttachments(keys, currentFBO.get());
#endif
}
void SyncNeccessaryTextures() { SyncNeccessaryTextures(CaptureDrawTextureSyncKeys()); }
@@ -2315,8 +2527,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Array<SyncedFramebufferSerialMemo, SizeT(FramebufferTarget::FramebufferTargetCount)>
g_fboSyncedSerials{};
// Whether the last SyncCurrentFBO found the two records describing the two bindings.
// BindCurrentFBO's handle arm reads it instead of re-asking, because the whole value
// of that arm is that it does NOT read the binding slot - and because every entry
// point that binds runs the sync first, immediately before (PrepareForDraw, Clear, the
// blits, the DSA clears, the readbacks). It starts false and every decline clears it,
// so a bind that somehow arrives without a preceding sync takes the pre-handle arm
// rather than trusting a record nothing checked.
static Bool g_fboRecordsTrusted = false;
static void InvalidateFramebufferHandleArmMemos() {
for (auto& memo : g_fboSyncedSerials) memo.valid = false;
g_fboRecordsTrusted = false;
}
// Record that `target` now reflects the applier's record as of `serial`. The two
@@ -2330,6 +2552,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
memo.valid = true;
}
// The seam check, and it is the reason this arm can be trusted at all on a tree where
// the client half is mid-landing. The record says which framebuffer each binding
// holds; the binding slot says the same thing. If they disagree, the record is stale
// or mis-keyed and driving the driver from it would sync - or skip - the wrong
// framebuffer, which is the one failure this path could produce that no pixel test on
// this tree could see. Disagreement DECLINES the whole arm rather than fixing up one
// target, because the two records are emitted together and one of them being wrong
// says nothing good about the other.
//
// The default framebuffer is compared through IsDefault rather than through Fbo,
// because the reserved handle {0,1} is not the handle the twin table minted for the
// default FramebufferObject; comparing it against HandleOf would fail for the one case
// the reserved handle exists to make easy. This is the identity comparison D-C1
// retires, kept here ONLY as a consistency check and only until package D's twin API
// takes the record instead of the object - at which point there is no second answer
// left to disagree with.
static Bool FramebufferRecordMatchesBinding(FramebufferTarget target,
const MG_Pipe::MGPFramebufferState& record) {
const auto& bound = GetFramebufferBindingSlotChecked(target).GetBoundObject();
const Bool boundIsDefault =
!bound || bound == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO;
if ((record.IsDefault != 0) != boundIsDefault) return false;
if (boundIsDefault) return true;
return record.Fbo == g_backendFramebufferObjects.HandleOf(bound.get());
}
static Bool SyncedFramebufferSerialIsCurrent(FramebufferTarget target, Uint64 serial) {
const auto& memo = g_fboSyncedSerials[SizeT(target)];
return memo.valid && memo.serial == serial &&
@@ -2354,11 +2602,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// state of a tree whose client half has not landed yet, and that state belongs to the
// pre-handle arm.
static Bool SyncCurrentFBOByRecord() {
g_fboRecordsTrusted = false;
const auto& st = MG_Pipe::MGPipeApplier();
if (MG_Pipe::MGPipeHandleIsNull(st.DrawFramebuffer.Fbo) ||
MG_Pipe::MGPipeHandleIsNull(st.ReadFramebuffer.Fbo)) {
return false;
}
if (!FramebufferRecordMatchesBinding(FramebufferTarget::Draw, st.DrawFramebuffer) ||
!FramebufferRecordMatchesBinding(FramebufferTarget::Read, st.ReadFramebuffer)) {
MGLOG_E_ONCE("A framebuffer record does not describe the binding it names; "
"running the pre-handle framebuffer sync.");
return false;
}
g_fboRecordsTrusted = true;
const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read};
for (auto& target : fboTargets) {
@@ -3314,7 +3570,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
{
Bool broadcastCountResolved = false;
#if MOBILEGL_PIPE_PUSH
if (EsprytDrawFramebufferHandlesEnabled()) {
// The trust latch is fresh here and costs nothing: PrepareForDraw runs
// SyncCurrentFBO immediately before this, so the records have just been
// checked against the two bindings.
if (EsprytDrawFramebufferHandlesEnabled() && FramebufferImpl::g_fboRecordsTrusted) {
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPFramebufferState& record = st.DrawFramebuffer;
if (!MG_Pipe::MGPipeHandleIsNull(record.Fbo)) {
@@ -3479,13 +3738,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// framebuffer" and "is it the default one" - are answered: the record's Fbo and its
// IsDefault byte, rather than the bound object's address and a comparison against
// pDefaultFramebufferInfo->defaultFBO.
if (EsprytDrawFramebufferHandlesEnabled()) {
if (EsprytDrawFramebufferHandlesEnabled() && FramebufferImpl::g_fboRecordsTrusted) {
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPFramebufferState& record =
target == FramebufferTarget::Draw ? st.DrawFramebuffer : st.ReadFramebuffer;
// The record's own handle is the "has this binding ever been described" test, not
// FramebufferSerial - which MGPipeApplierReset advances whether or not anything
// was ever emitted (see SyncCurrentFBOByRecord).
// was ever emitted (see SyncCurrentFBOByRecord). The trust latch above is the
// other half: it says the sync that ran a moment ago found these two records
// describing these two bindings.
if (!MG_Pipe::MGPipeHandleIsNull(record.Fbo)) {
if (record.IsDefault == 0) {
auto* twinEntry = FramebufferImpl::g_backendFramebufferObjects.FindByHandle(record.Fbo);