[MG_Backend] key the handle arm's attachment walk on the object, not the binding

SyncToBackendByHandle gated its record-driven attachment walk (the eleven
applyPoint calls plus NoteFramebufferTextureAttachments) on
m_syncedRecordHashes[asTarget], a PER-TARGET slot, while the work inside is
object-level: glFramebufferTexture2D/glFramebufferRenderbuffer write the
driver FBO behind whichever binding is current, so a Draw walk and a Read walk
write the same points. Two slots can therefore hold two individually true,
jointly false accounts of one physical state.

improved-transparency flips a composite framebuffer between two attachment
states and syncs one of them as DRAW in a depth pre-pass. That walk detaches
the colour point, stamps the Draw slot; the end-of-frame READ-side sync of the
other record then found its own slot already stamped by an earlier identical
sync, walked nothing, left the point detached, and the driver refused the
following glBlitFramebuffer with INVALID_OPERATION - the default framebuffer
froze from frame 835. The object form is immune because a per-point version
array gates it underneath; the handle arm has no frontend array, so this memo
is its only gate.

Replace it there with m_syncedAttachmentRecordHash, one slot for the object.
The per-target array stays for the object arm, where it is a re-arm trigger
over that version array rather than a claim. InvalidateSyncedState now zeroes
both memos: it invalidates the version array, which is not the handle arm's
gate, so a memo left standing would let a re-sync of the same record skip the
re-attach that call exists to force.
This commit is contained in:
2026-09-20 19:36:16 -04:00
parent 7059869848
commit e004269d96
2 changed files with 52 additions and 17 deletions
+21 -5
View File
@@ -9872,8 +9872,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
std::fill(std::begin(m_backendDrawBuffers), std::end(m_backendDrawBuffers), GL_NONE);
// NOTE: this does NOT empty the backend ES framebuffer - m_backendFBOId keeps every
// attachment it had, possibly under a non-identity permutation. Declaring the table
// identity here is safe only because every attachment version below is invalidated too,
// so the next sync re-attaches all non-empty attachments at their identity points AND
// identity here is safe only because every gate that decides which points get
// re-issued is invalidated too - the attachment versions below, and under
// MOBILEGL_PIPE_PUSH the record memos stamped at the end of this function - so the
// next sync re-attaches all non-empty attachments at their identity points AND
// (see SyncToBackend's attachment loop) detaches any colour point whose frontend owner
// is empty. Without that detach a stale image would survive under a point the table now
// claims for a different, empty attachment.
@@ -9887,6 +9889,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Every attachment version is invalidated above, so the next walk re-attaches
// everything regardless; stamp the generation so it does not re-arm twice.
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
#if MOBILEGL_PIPE_PUSH
// The record memos are the gate the PIPE_PUSH arms' attachment walks actually read -
// the handle arm has nothing narrower under its key - so leaving them standing here
// would let a re-sync of the same record skip the re-attach this function exists to
// force. 0 is never a live hash, so a zeroed memo is a guaranteed miss.
m_syncedRecordHashes = {0};
m_syncedAttachmentRecordHash = 0;
#endif
}
static Bool SyncAttachmentObject(GLenum glFBOTarget,
@@ -11345,13 +11355,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
ApplyReadBufferFromRecord(record, 0);
}
// 3. The attachments. Two gates, both server-owned.
// 3. The attachments. Three gates, and the key is the object's rather than the
// binding's: idGenerationMoved ("I re-minted a driver id, so a point may hold a
// dead one"), attachmentPointsMoved ("the colour permutation moved, so a point
// whose record did not change still has to be re-issued") and the record hash.
// NOT m_syncedRecordHashes: the per-target array is the object arm's re-arm
// trigger, and the points written below belong to the driver FBO rather than to
// the binding they were walked for - see m_syncedAttachmentRecordHash.
const Bool idGenerationMoved = m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration;
if (idGenerationMoved) {
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
}
if (idGenerationMoved || attachmentPointsMoved ||
m_syncedRecordHashes[SizeT(asTarget)] != record.ContentHash) {
m_syncedAttachmentRecordHash != record.ContentHash) {
const auto applyPoint = [&](FramebufferAttachmentType point,
const MG_Pipe::MGPSurface& surface) {
const Bool isColorPoint = point >= FramebufferAttachmentType::Color0 &&
@@ -11388,7 +11404,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
applyPoint(FramebufferAttachmentType::Depth, record.Depth);
applyPoint(FramebufferAttachmentType::Stencil, record.Stencil);
m_syncedRecordHashes[SizeT(asTarget)] = record.ContentHash;
m_syncedAttachmentRecordHash = record.ContentHash;
NoteFramebufferTextureAttachments(fbo, record);
}