[MG_Backend] (Disaggregated): P5e fb - the FBO doors, both attachment lists, the binds, the blit and the detach walk read records

- The doors: RefuseFramebufferBindingSlotRead raises
  Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@<verb>"} where a decline used
  to fall through to the pre-handle arm. Under a transport with the framebuffer bit
  set the fallback is not a safe default - it would read a row belonging to a client
  this apply is no longer synchronous with - so SyncCurrentFBO's and BindCurrentFBO's
  declines become the detector CONTRACT-P5E §5.4 asks for.
- The two attachment texture lists are the package's core: the one unconditional
  per-draw live read of this family, taken whether the memo hit or missed. They are
  now keyed {Fbo, ContentHash, ContextSerial, g_backendContextGeneration} with entries
  {Res, twin} that borrow nothing, and the dedupe asks the two BOUND HANDLES rather
  than two pointers. Three frontend reads go in one move: both binding slots, the
  FramebufferObject the entries borrowed their attachment slots from, and
  GetTextureContextId.
- SyncCurrentFBOByRecord drives the twin through SyncToBackendByHandle /
  SyncReadBufferToBackendByHandle; NoteStateForHandle is gone, which was the server's
  last cross-record hold on a frontend framebuffer.
- FramebufferRecordMatchesBinding is monolith-only: the record was resolved from the
  applier's own bound handle, so there is no second answer left to corroborate.
- ForceBindCurrentFBO reads the binding slot INSIDE the else instead of on both sides
  of its own handle arm (S3 finding #7); the three legacy stamps go with it, which is
  safe for the reason the serial memo is invalidated rather than stamped there.
- SyncAndBindFramebufferByHandle configures from the record, so a DSA framebuffer that
  was never bound is configured correctly instead of being named and bound unconfigured.
- Both blit arms take two const MGPFramebufferState&: draw buffers from DrawBuffers[],
  the read buffer by matching ReadSurface against Color[], kind/level/layer/layered/
  format from the surface, Samples from the record, the textures by handle.
- The detach walk uses the reverse index and the records instead of ForEachLive +
  StateForHandle + the frontend attachment array. The frontend-keyed scope stays at
  this site: every caller reaches it from a barriered row (§4.4).
- SyncRenderbufferObjectToBackend refuses by name under a transport - its only caller
  is the copy-image endpoint, whose renderbuffer form both the emitter and the sink
  already refuse - and the scope at that site is deleted.
- Clear's debug-only draw-FBO diagnostic reads the record under a transport.
This commit is contained in:
2026-09-18 08:02:24 -04:00
parent 3012d52e2b
commit 6c37c20f59
3 changed files with 487 additions and 115 deletions
+474 -113
View File
@@ -2570,7 +2570,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.
#if MOBILEGL_BUILD_DISAGGREGATED
#if MOBILEGL_PIPE_PUSH && MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb): under a transport with the framebuffer bit set, BOTH attachment lists
// are answered from the two records and neither binding slot is touched. The
// pre-handle pair below stays compiled and stays the monolith path (ruling 1).
@@ -3261,8 +3261,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
//
// P5e (fb, CONTRACT-P5E.md §5.4): MONOLITH-ONLY. The sentence above ends "...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". That point is this package: the
// twin's sync takes the handle, the record was resolved from the applier's OWN bound
// handle, and the only thing this could still consult is the binding slot - a
// BARRIER_PULLED row an apply thread may not read. So under a transport the check does
// not run at all, rather than running on a value it is not allowed to have.
static Bool FramebufferRecordMatchesBinding(FramebufferTarget target,
const MG_Pipe::MGPFramebufferState& record) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (FramebufferRecordArmIsMandatory()) return true;
#endif
const auto& bound = GetFramebufferBindingSlotChecked(target).GetBoundObject();
const Bool boundIsDefault =
!bound || bound == MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO;
@@ -3388,11 +3399,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
// MONOLITH GLUE, and named as such: the twin's SyncToBackend still reads the
// frontend object (package D re-keys its BODY onto the record; this package
// owns the call site and the decisions around it). The IDENTITY is already the
// handle - FindByHandle(record.Fbo) - so a recycled FBO cannot be mistaken for
// its predecessor here.
// MONOLITH GLUE, and named as such: on the monolith arm the twin's SyncToBackend
// still reads the frontend object, and the bound object is still this call's
// ARGUMENT. The IDENTITY is already the handle - GetOrCreateByHandle(record.Fbo) -
// so a recycled FBO cannot be mistaken for its predecessor here.
//
// A1, TAKEN AT THE VERIFICATION ROUND AGAINST WHAT D ACTUALLY BUILT. The twin
// is resolved BY HANDLE ONLY: `BackendPtr* GetOrCreateByHandle(MGPipeHandle)`
@@ -3400,47 +3410,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
// GetOrCreate(StatePtr) returns, deliberately, because it has three ways to
// decline - the legacy arm, a slot past the table's sanity bound, and a
// generation BEHIND the live entry's - and every one of them has to be visible
// here rather than answered with a parked twin. So the FindByHandle-then-
// GetOrCreate(currentFBO) pair is gone and with it the last place on this arm
// where a twin could be minted against the bound frontend ADDRESS.
// here rather than answered with a parked twin.
//
// WHAT DID NOT GO, against the review's A1 sketch, and why:
// * `slot.GetBoundObject()` STAYS. A2's `#else` half never fired - D left
// SyncToBackend(SharedPtr<FramebufferObject>, target) and
// SyncReadBufferToBackend(SharedPtr<FramebufferObject>) textually
// unchanged - so the frontend object is still this call's ARGUMENT. Only
// the twin LOOKUP moved to the handle.
// * FramebufferRecordMatchesBinding (F3) STAYS, and the §2 table's "keep
// verbatim; it is the wording §8.2 greps" is the ruling that governs: with
// the twin now resolved from the record and configured FROM THE BOUND
// OBJECT, the identity check is the only thing standing between a
// mis-keyed record and one framebuffer's attachments written into
// another's twin. Removing it would delete the check that makes this
// rewrite safe.
// P5e (fb, CONTRACT-P5E.md §5.4): AND ON THE RECORD ARM THE OBJECT IS GONE TOO.
// `SyncToBackendByHandle(record.Fbo, target)` takes the record's own handle, so
// there is no binding slot to read, no object to hand over, and no state note
// to leave behind - NoteStateForHandle was the server's last cross-record hold
// on a frontend framebuffer, and the named blit that needed it now reads the
// record too. What is left frontend under a transport is nothing at all.
//
// THE POINTER-INVALIDATION CONTRACT (Managers.h ~381-385): a handle-arm result
// is a stable array element that only a table-GROWING GetOrCreate can move.
// GetOrCreateByHandle is exactly such a call, so its result is used and dropped
// inside this iteration and never held across another registry call.
auto& slot = GetFramebufferBindingSlotChecked(target);
const auto& currentFBO = slot.GetBoundObject();
// P4a decline-site F4: S - FLIPPED AT THE VERIFICATION ROUND to the FULL
// fallback the review specified. Still unreachable in practice
// (FramebufferRecordMatchesBinding maps "nothing bound" to boundIsDefault and
// a non-default record is already rejected above), but the `continue` it used
// to take would have left the other target half-run against section 1's
// invariant that a decline is a whole-arm decline.
if (!currentFBO) {
MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the "
"%s record names {slot %u, gen %u} but no FBO is bound to that "
"binding; running the pre-handle framebuffer sync.",
target == FramebufferTarget::Read ? "READ" : "DRAW",
static_cast<unsigned>(record.Fbo.Slot),
static_cast<unsigned>(record.Fbo.Gen));
g_fboRecordsTrusted = false;
return false;
}
auto* const twinSlot = g_backendFramebufferObjects.GetOrCreateByHandle(record.Fbo);
// A1's third decline. Bit 9 implies bit 10 implies bit 7, so the slot tables are
// armed whenever this function runs and the legacy-arm cause cannot fire here;
@@ -3465,28 +3447,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!backendObj) {
backendObj = MakeShared<BackendFramebufferObject>();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): with an active transport the twin's sync keys its applier-record
// lookup on the record's own handle (m_pushedSyncHandle), never on the client
// allocator (T2), and the table remembers which frontend object the twin is
// synced from so a later handle-only resolution (the named blit) can reach it.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
backendObj->m_pushedSyncHandle = record.Fbo;
g_backendFramebufferObjects.NoteStateForHandle(record.Fbo, currentFBO);
}
#endif
// THE "SAME FBO AS DRAW" SKIP IS NOW A FIELD, not a pointer comparison against
// the object the previous iteration happened to sync. Target = Both is the
// client saying one object is bound to both bindings, so the attachment and
// THE "SAME FBO AS DRAW" SKIP IS A FIELD, not a pointer comparison against the
// object the previous iteration happened to sync. Target = Both is the client
// saying one object is bound to both bindings, so the attachment and
// draw-buffer work the DRAW pass already did is not repeated - and the read
// buffer, which is READ-target-specific and is what that skip used to drop, is
// applied unconditionally on this path.
//
// MINOR-1, CORRECTED: the read buffer is applied by SyncReadBufferToBackend
// FROM THE FRONTEND OBJECT, not from MGPFramebufferState::ReadSurface - this
// package reads Fbo, IsDefault, DrawBuffers[] and ContentHash and nothing else.
//
// ID-27 (wire review v2, MAJOR-1): THE QUESTION IS ASKED OF THE BOUND HANDLES,
// NOT OF THE RECORD'S STORED TARGET, and it is no longer possible to ask it any
// other way in this file. `MGPFramebufferState::Target` is the target of the
@@ -3499,15 +3467,53 @@ namespace MobileGL::MG_Backend::DirectGLES {
// are non-null here (F2 rejected a null on either), and a handle compares by
// {slot, gen}, so a recycled slot is not its predecessor.
//
// Reading the stored Target degraded to a redundant per-frame read-buffer sync
// rather than to a wrong picture, which is exactly why it needed replacing here
// instead of being caught by a lane: `Target != Both` takes the full
// SyncToBackend path below, which is correct and merely repeats the draw pass's
// attachment work. The wire review found it by reading the emitter.
// MINOR-1's correction is RETIRED by P5e: the read buffer is no longer applied
// "FROM THE FRONTEND OBJECT" on this arm - ApplyReadBufferFromRecord resolves it
// out of MGPFramebufferState::ReadSurface, which was already the record's
// answer, and the by-handle entry is what reaches it here.
const auto& boundHandles = st.BoundFramebuffer;
if (target == FramebufferTarget::Read &&
const Bool sameObjectOnBothBindings =
target == FramebufferTarget::Read &&
boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)] ==
boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)]) {
boundHandles[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)];
#if MOBILEGL_BUILD_DISAGGREGATED
if (FramebufferRecordArmIsMandatory()) {
if (sameObjectOnBothBindings) {
backendObj->SyncReadBufferToBackendByHandle(record.Fbo);
} else {
backendObj->SyncToBackendByHandle(record.Fbo, target);
}
StampSyncedFramebufferSerial(target, st.FramebufferSerial);
continue;
}
#endif
auto& slot = GetFramebufferBindingSlotChecked(target);
const auto& currentFBO = slot.GetBoundObject();
// P4a decline-site F4: S - FLIPPED AT THE VERIFICATION ROUND to the FULL
// fallback the review specified. Still unreachable in practice
// (FramebufferRecordMatchesBinding maps "nothing bound" to boundIsDefault and
// a non-default record is already rejected above), but the `continue` it used
// to take would have left the other target half-run against section 1's
// invariant that a decline is a whole-arm decline.
if (!currentFBO) {
MGLOG_E_ONCE("A framebuffer record does not describe the binding it names: the "
"%s record names {slot %u, gen %u} but no FBO is bound to that "
"binding; running the pre-handle framebuffer sync.",
target == FramebufferTarget::Read ? "READ" : "DRAW",
static_cast<unsigned>(record.Fbo.Slot),
static_cast<unsigned>(record.Fbo.Gen));
g_fboRecordsTrusted = false;
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the push-monolith twin keys its applier-record lookup on the
// record's own handle (m_pushedSyncHandle) rather than on the client allocator.
// The state note is GONE with P5e: nothing resolves a framebuffer by handle and
// then asks the table for an object any more.
backendObj->m_pushedSyncHandle = record.Fbo;
#endif
if (sameObjectOnBothBindings) {
backendObj->SyncReadBufferToBackend(currentFBO);
StampSyncedFramebufferSerial(target, st.FramebufferSerial);
continue;
@@ -3536,6 +3542,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
// IS D's FramebufferSubsystemEnabled() now. Silent, confirmed at the
// verification round.
if (FramebufferSubsystemEnabled() && SyncCurrentFBOByRecord()) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, CONTRACT-P5E.md §5.4): AND UNDER A TRANSPORT THE DECLINE IS THE END OF
// IT. The fallback below reads GetFramebufferBindingSlot, a BARRIER_PULLED row; the
// value it would read belongs to a client this apply is no longer synchronous with,
// so "run the pre-handle sync" stops being a safe default and becomes a wrong
// picture taken from torn state. Every cause of the decline is already named by the
// MGLOG_E_ONCE that produced it (F2's half-described / neither-described applier,
// F4's unbound binding, A1's refused handle); this turns that log into the abort the
// contract asks for, with the verb in the message so the strict lane's marker table
// points at the site.
if (FramebufferRecordArmIsMandatory()) {
RefuseFramebufferBindingSlotRead();
}
#endif
#endif
const FramebufferTarget fboTargets[] = {FramebufferTarget::Draw, FramebufferTarget::Read};
@@ -4810,6 +4830,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, §5.4): the pre-handle arm below reads the binding slot, so under a transport
// reaching it is the same refusal SyncCurrentFBO takes. Reaching it means either the
// sync's trust latch is clear - and the sync aborted before it could be - or this bind
// ran with no sync in front of it, which the latch exists to catch.
if (FramebufferRecordArmIsMandatory()) {
RefuseFramebufferBindingSlotRead();
}
#endif
#endif
auto& slot = GetFramebufferBindingSlotChecked(target);
@@ -4944,18 +4973,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
backendObj = MakeShared<FramebufferImpl::BackendFramebufferObject>();
}
backendObj->m_pushedSyncHandle = fbo;
const auto stateObj = registry.StateForHandle(fbo);
if (stateObj) {
if (forceSync) {
backendObj->InvalidateSyncedState();
}
backendObj->SyncToBackend(stateObj, target);
} else {
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no frontend object noted on "
"this side - it was never synced through a binding, so its twin is "
"bound unconfigured",
fbo.Slot, fbo.Gen);
// P5e (fb, CONTRACT-P5E.md §5.4): THE STATE NOTE IS GONE. The paragraph above described
// a handle arm that still had to find "the frontend object the sync body walks", and
// its failure mode - a framebuffer named by a DSA entry point that was never bound, so
// no note exists and the twin is bound unconfigured - was the last consequence of that
// detour. SyncToBackendByHandle configures the twin from the RECORD, which every named
// framebuffer has by construction (a Named record precedes every DSA site, ID-19), so
// the never-bound case is now configured correctly rather than named and refused.
if (forceSync) {
backendObj->InvalidateSyncedState();
}
backendObj->SyncToBackendByHandle(fbo, target);
backendObj->Bind(target);
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
@@ -4964,12 +4992,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& slot = GetFramebufferBindingSlotChecked(target);
const auto& fbo = slot.GetBoundObject();
#if MOBILEGL_BUILD_DISAGGREGATED
#if MOBILEGL_PIPE_PUSH && MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): with an active transport the bound framebuffer's handle is the applier's
// own working state; the object form's registry probe never runs (T2).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && FramebufferSubsystemEnabled()) {
//
// P5e (fb, CONTRACT-P5E.md §5.4): AND THE BINDING SLOT IS READ INSIDE THE ELSE, NOT
// BEFORE THE IF. The read and the three stamps below used to straddle this arm - S3's
// finding #7 - so the handle path still touched a BARRIER_PULLED row on both sides of
// the one branch that existed to avoid it. The stamps are the LEGACY memo's
// (g_fboSynced*), and skipping them here is safe for exactly the reason the serial memo
// below is invalidated rather than stamped: nothing on this path consulted the frontend
// trio, so the honest thing is to leave it invalid and pay one extra sync. Under a
// transport the legacy arm is never taken anyway (SyncCurrentFBO refuses first), so the
// memo it feeds has no reader.
if (FramebufferRecordArmIsMandatory()) {
SyncAndBindFramebufferByHandle(
MG_Pipe::MGPipeApplier().BoundFramebuffer[static_cast<SizeT>(
target == FramebufferTarget::Read ? MG_Pipe::MGPipeFramebufferTarget::Read
@@ -4977,12 +5013,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
target);
} else
#endif
SyncAndBindFramebufferObject(fbo, target);
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
FramebufferImpl::g_fboSyncedObjectVersions[(SizeT)target] = fbo ? fbo->GetObjectVersion() : 0;
FramebufferImpl::g_fboSyncedObjects[(SizeT)target] = fbo.get();
FramebufferImpl::g_fboSyncedBackendIdGenerations[(SizeT)target] =
FramebufferImpl::g_attachmentBackendIdGeneration;
{
auto& slot = GetFramebufferBindingSlotChecked(target);
const auto& fbo = slot.GetBoundObject();
SyncAndBindFramebufferObject(fbo, target);
FramebufferImpl::g_fboSyncedSlotVersions[(SizeT)target] = slot.GetVersion();
FramebufferImpl::g_fboSyncedObjectVersions[(SizeT)target] = fbo ? fbo->GetObjectVersion() : 0;
FramebufferImpl::g_fboSyncedObjects[(SizeT)target] = fbo.get();
FramebufferImpl::g_fboSyncedBackendIdGenerations[(SizeT)target] =
FramebufferImpl::g_attachmentBackendIdGeneration;
}
#if MOBILEGL_PIPE_PUSH
// P4a decline-site F9 / MINOR-3: THE HANDLE ARM'S MEMO IS INVALIDATED HERE, NOT
// STAMPED, and it is done per target for the reason the memo is kept per target at
@@ -6527,15 +6567,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLfloat rb[4] = {0};
g_GLESFuncs.glReadPixels(100, 100, 1, 1, GL_RGBA, GL_FLOAT, rb);
const GLenum rbErr = g_GLESFuncs.glGetError();
const auto& feFbo =
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
int feDb0 = -1, feDb1 = -1;
Uint feIdx = 0, feVer = 0;
if (feFbo) {
feIdx = feFbo->GetExternalIndex();
feVer = feFbo->GetObjectVersion();
feDb0 = (int)feFbo->GetDrawBuffers()[0];
feDb1 = (int)feFbo->GetDrawBuffers()[1];
#if MOBILEGL_PIPE_PUSH && MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb): DEBUG-ONLY, and still a BARRIER_PULLED read. A diagnostic
// is not a reason to touch the binding slot on an apply thread, so
// under a transport the same four numbers come off the draw record -
// the external index has no record counterpart and stays 0, which reads
// as "the record did not say".
if (FramebufferRecordArmIsMandatory()) {
if (const auto* rec = BoundFramebufferRecord(
FramebufferTarget::Draw)) {
feVer = static_cast<Uint>(rec->ContentHash);
feDb0 = (int)rec->DrawBuffers[0];
feDb1 = (int)rec->DrawBuffers[1];
}
} else
#endif
{
const auto& feFbo =
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject();
if (feFbo) {
feIdx = feFbo->GetExternalIndex();
feVer = feFbo->GetObjectVersion();
feDb0 = (int)feFbo->GetDrawBuffers()[0];
feDb1 = (int)feFbo->GetDrawBuffers()[1];
}
}
MGLOG_D("CLEARV fbo=%d clrErr=0x%x rbErr=0x%x cc.x=%g cleared=%d firstDb=0x%x prevReadBuf=0x%x "
"feFbo=%u feVer=%u feDb=[%d,%d] stored=(%g,%g,%g,%g)",
@@ -8566,6 +8623,212 @@ namespace MobileGL::MG_Backend::DirectGLES {
return handled & mask;
}
#if MOBILEGL_PIPE_PUSH
// ---- P5e (fb, CONTRACT-P5E.md §5.4): the same aspect plan, from the two RECORDS ---------
//
// The blit was the last entry point in this family that still reached into two frontend
// FramebufferObjects on every call - GetDrawBuffers, GetReadBuffer, GetAttachment and then
// six texture properties per aspect - and it did it on BOTH arms, the named one reaching
// the objects through the twin table's state note. Every one of those questions is a field
// of MGPFramebufferState:
//
// GetDrawBuffers() -> DrawBuffers[8] (attachment index, -1 = None)
// GetReadBuffer() -> ReadSurface matched against Color[] (the same match
// SyncReadBufferToBackend makes; ReadSurface is resolved
// from THIS framebuffer's own read buffer under every
// Target, Named included)
// GetAttachment(point) -> PushedSurfaceForAttachment
// IsTexture / IsLayered -> Kind / Layered
// GetTextureLevel / Layer -> Level / Layer
// GetFormat -> InternalFormat (the attachment's, which is the texture's)
// GetSamples -> record.Samples
//
// SAMPLES COMES OFF THE RECORD RATHER THAN OFF EACH TEXTURE, and that is a deliberate
// narrowing: the frontend form asked each endpoint texture, the record states it per
// FRAMEBUFFER. A framebuffer whose attachments disagree about sample count is incomplete,
// so the two cannot differ where this substitution is allowed to fire; and the test the
// answer feeds ("either end is multisample -> leave it to the driver") is conservative in
// the direction that hands the call back.
static FramebufferAttachmentType PushedReadBufferPoint(const MG_Pipe::MGPFramebufferState& record) {
if (MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res)) return FramebufferAttachmentType::None;
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
const auto& color = record.Color[i];
if (color.Res == record.ReadSurface.Res && color.Kind == record.ReadSurface.Kind &&
color.Layered == record.ReadSurface.Layered && color.Level == record.ReadSurface.Level &&
color.Layer == record.ReadSurface.Layer &&
color.UploadTarget == record.ReadSurface.UploadTarget) {
return static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i));
}
}
return FramebufferAttachmentType::None;
}
static GLbitfield BlitLayeredDestinationAspects(const MG_Pipe::MGPFramebufferState& readRecord,
const MG_Pipe::MGPFramebufferState& drawRecord, GLint srcX0,
GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0,
GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask) {
if (mask == 0) return 0;
if (!g_GLESFuncs.glCopyImageSubData) return 0;
if (!MG_Util::SelfTest::BlitIgnoresDestinationArrayLayer(g_GLESFuncs)) return 0;
// The default framebuffer has no layers to get wrong.
if (readRecord.IsDefault != 0 || drawRecord.IsDefault != 0) return 0;
const Int width = srcX1 - srcX0;
const Int height = srcY1 - srcY0;
const Bool oneToOne = width > 0 && height > 0 && (dstX1 - dstX0) == width && (dstY1 - dstY0) == height;
// The scissor clips a blit and does not clip a copy, so an enabled scissor makes the two
// different operations no matter how the rectangles line up.
const Bool scissorEnabled =
(RenderStateImpl::g_syncedRenderStateParameters.ScissorTestEnabledMask & 1u) != 0;
using MobileGL::FramebufferAttachmentType;
struct AspectPlan {
GLbitfield bit;
FramebufferAttachmentType source;
FramebufferAttachmentType destination;
};
// The buffer is FOUND rather than assumed to be slot 0, for the frontend form's reason:
// a blit writes every ENABLED draw buffer, and glDrawBuffers(NONE, NONE, NONE,
// COLOR_ATTACHMENT0) leaves slot 0 empty while still naming exactly one destination.
// -1 is the record's spelling of GL_NONE and is not "no such attachment".
Int enabledDrawBuffers = 0;
FramebufferAttachmentType colorDestination = FramebufferAttachmentType::None;
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
const Int8 index = drawRecord.DrawBuffers[i];
if (index < 0) continue;
++enabledDrawBuffers;
if (enabledDrawBuffers == 1) {
colorDestination = static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(index));
}
}
const AspectPlan plans[] = {
{GL_COLOR_BUFFER_BIT, PushedReadBufferPoint(readRecord), colorDestination},
{GL_DEPTH_BUFFER_BIT, FramebufferAttachmentType::Depth, FramebufferAttachmentType::Depth},
{GL_STENCIL_BUFFER_BIT, FramebufferAttachmentType::Stencil, FramebufferAttachmentType::Stencil},
};
GLbitfield handled = 0;
for (const AspectPlan& plan : plans) {
if ((mask & plan.bit) == 0) continue;
if (plan.source == FramebufferAttachmentType::Unknown ||
plan.destination == FramebufferAttachmentType::Unknown ||
plan.source == FramebufferAttachmentType::None ||
plan.destination == FramebufferAttachmentType::None) {
continue;
}
const MG_Pipe::MGPSurface* const sourceSurface =
FramebufferImpl::PushedSurfaceForAttachment(readRecord, plan.source);
const MG_Pipe::MGPSurface* const destinationSurface =
FramebufferImpl::PushedSurfaceForAttachment(drawRecord, plan.destination);
if (sourceSurface == nullptr || destinationSurface == nullptr) continue;
// Renderbuffers have no layers, so a destination that is one cannot be hitting this.
if (sourceSurface->Kind != MG_Pipe::kMGPipeSurfaceKindTexture ||
destinationSurface->Kind != MG_Pipe::kMGPipeSurfaceKindTexture) {
continue;
}
if (MG_Pipe::MGPipeHandleIsNull(sourceSurface->Res) ||
MG_Pipe::MGPipeHandleIsNull(destinationSurface->Res)) {
continue;
}
// Layer 0 is the case the driver gets right, and a LAYERED attachment
// (glFramebufferTexture with no layer) blits its layer 0 by spec - neither is this
// defect.
if (destinationSurface->Layer == 0) continue;
if (destinationSurface->Layered != 0 || sourceSurface->Layered != 0) continue;
// glCopyImageSubData moves texel blocks: same format both ends, or it is a different
// operation. Multisample endpoints would additionally have to agree on sample count,
// which is a resolve the driver still owns.
if (sourceSurface->InternalFormat != destinationSurface->InternalFormat) continue;
if (readRecord.Samples > 0 || drawRecord.Samples > 0) continue;
// Copying an image region onto itself is undefined for glCopyImageSubData, and a blit
// whose source and destination overlap is undefined for GL too - so this is not a
// shape to substitute FOR, it is one to leave exactly as the application wrote it.
if (sourceSurface->Res == destinationSurface->Res &&
sourceSurface->Level == destinationSurface->Level &&
sourceSurface->Layer == destinationSurface->Layer) {
continue;
}
// A combined depth-stencil texture is ONE image to glCopyImageSubData: it carries both
// aspects across whether or not the mask asked for both. Taking only GL_DEPTH_BUFFER_BIT
// on a DEPTH24_STENCIL8 destination would overwrite a stencil the application asked to
// keep, so the copy is only allowed when the mask covers everything the format holds.
const auto format = static_cast<TextureInternalFormat>(destinationSurface->InternalFormat);
const Bool hasDepth = MG_Util::IsDepthFormatInternalFormat(format);
const Bool hasStencil = MG_Util::IsStencilFormatInternalFormat(format);
if (hasDepth && (mask & GL_DEPTH_BUFFER_BIT) == 0) continue;
if (hasStencil && (mask & GL_STENCIL_BUFFER_BIT) == 0) continue;
// ... and having carried both, it must be credited with both, or the caller hands the
// stencil half to the driver and it lands on layer 0 after all.
const GLbitfield aspectBits =
hasDepth || hasStencil
? static_cast<GLbitfield>((hasDepth ? GL_DEPTH_BUFFER_BIT : 0) |
(hasStencil ? GL_STENCIL_BUFFER_BIT : 0))
: static_cast<GLbitfield>(GL_COLOR_BUFFER_BIT);
if ((handled & aspectBits) == aspectBits) continue;
if (!oneToOne || scissorEnabled || (plan.bit == GL_COLOR_BUFFER_BIT && enabledDrawBuffers != 1)) {
MGLOG_E_ONCE("BlitFramebuffer: this driver ignores a non-zero destination array layer and this "
"blit cannot be expressed as a copy (%s), so it will land on layer 0",
!oneToOne ? "it scales or flips"
: scissorEnabled ? "the scissor test is enabled"
: "the destination has more than one draw buffer");
continue;
}
auto& backendSource =
TextureImpl::SyncTextureToBackendByHandle(sourceSurface->Res,
/*imageBindableStorageRequired=*/false);
if (!backendSource) continue;
const GLuint sourceName = backendSource->GetBackendTextureId();
// BY VALUE past this point, for the reason the copy-image endpoint builder gives:
// the second sync can grow the registry and relocate the first result.
auto& backendDestinationSlot =
TextureImpl::SyncTextureToBackendByHandle(destinationSurface->Res,
/*imageBindableStorageRequired=*/false);
if (!backendDestinationSlot) continue;
const GLuint destinationName = backendDestinationSlot->GetBackendTextureId();
if (sourceName == 0 || destinationName == 0) continue;
const GLenum sourceTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(
static_cast<TextureTarget>(sourceSurface->TextureTarget));
const GLenum destinationTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(
static_cast<TextureTarget>(destinationSurface->TextureTarget));
ClearGLErrors();
g_GLESFuncs.glCopyImageSubData(sourceName, sourceTarget, static_cast<GLint>(sourceSurface->Level),
srcX0, srcY0, static_cast<GLint>(sourceSurface->Layer),
destinationName, destinationTarget,
static_cast<GLint>(destinationSurface->Level), dstX0, dstY0,
static_cast<GLint>(destinationSurface->Layer), width, height, 1);
if (const GLenum error = g_GLESFuncs.glGetError(); error != GL_NO_ERROR) {
// The driver blit still runs for this aspect - onto the wrong layer, but the
// substitute has to leave the call no worse off than it found it.
MGLOG_E_ONCE("BlitFramebuffer: the layered-destination copy substitute failed with %s; the "
"driver blit will run instead and land on layer 0",
MG_Util::ConvertGLEnumToString(error).c_str());
continue;
}
handled |= aspectBits;
}
return handled & mask;
}
// The two endpoint records of a blit, or nulls. `fbo` null-or-default means "the record of
// whatever is bound to that target", which is what the bound arm asks; a named blit passes
// the handles its record carried.
static const MG_Pipe::MGPFramebufferState* BlitEndpointRecord(MG_Pipe::MGPipeHandle fbo,
FramebufferTarget boundTarget) {
const auto& st = MG_Pipe::MGPipeApplier();
if (MG_Pipe::MGPipeHandleIsNull(fbo)) {
return boundTarget == FramebufferTarget::Read ? st.ReadFramebuffer() : st.DrawFramebuffer();
}
return st.FramebufferRecordFor(fbo);
}
#endif // MOBILEGL_PIPE_PUSH
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter) {
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER
@@ -8595,23 +8858,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
readFbo.Slot, readFbo.Gen, drawFbo.Slot, drawFbo.Gen, srcX0, srcY0, srcX1,
srcY1, dstX0, dstY0, dstX1, dstY1, mask,
MG_Util::ConvertGLEnumToString(filter).c_str());
// See the bound arm below: only the probed defect makes this do anything. It
// reads the two frontend objects' attachments (the object-class channel P3b/P4b
// retires), reached through the twins' state notes - and for a default endpoint
// the default framebuffer object itself, exactly as the bound arm's binding-slot
// read hands it. A missing object only skips the workaround.
const auto objectFor = [](MG_Pipe::MGPipeHandle handle) {
if (handle == MG_Pipe::kMGPipeDefaultFramebuffer) {
return SharedPtr<MG_State::GLState::FramebufferObject>(
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo->defaultFBO);
}
return FramebufferImpl::g_backendFramebufferObjects.StateForHandle(handle);
};
const auto readObj = objectFor(readFbo);
const auto drawObj = objectFor(drawFbo);
if (readObj && drawObj) {
mask &= ~BlitLayeredDestinationAspects(readObj, drawObj, srcX0, srcY0, srcX1, srcY1,
dstX0, dstY0, dstX1, dstY1, mask);
// P5e (fb, §5.4): only the probed defect makes this do anything, and it now
// asks the two RECORDS. The state-note detour this used to take - the twin
// table handing back the frontend object it was synced from, plus
// pDefaultFramebufferInfo->defaultFBO for a default endpoint - is gone with the
// note itself; a named framebuffer always has a record (a Named record precedes
// every DSA site, ID-19), so the "missing object skips the workaround" case is
// now a missing record and means the same thing.
const auto* readRecord = BlitEndpointRecord(readFbo, FramebufferTarget::Read);
const auto* drawRecord = BlitEndpointRecord(drawFbo, FramebufferTarget::Draw);
if (readRecord != nullptr && drawRecord != nullptr) {
mask &= ~BlitLayeredDestinationAspects(*readRecord, *drawRecord, srcX0, srcY0, srcX1,
srcY1, dstX0, dstY0, dstX1, dstY1, mask);
}
if (mask != 0) {
IssueBlitWithResolveFallback(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
@@ -8651,6 +8909,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
dstX1, dstY1, mask, MG_Util::ConvertGLEnumToString(filter).c_str());
// A no-op on every driver that honours a non-zero destination array layer, which is all
// of them but the probed one. Whatever it performs itself is taken out of the mask.
#if MOBILEGL_PIPE_PUSH && MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, §5.4): the bound arm's two binding-slot reads were the last GetBoundObject
// pair on this path; under a transport the two bound RECORDS answer instead
// (FramebufferRecordFor(st.BoundFramebuffer[t]), through BlitEndpointRecord's null
// case). SyncCurrentFBO ran a few lines up and refuses if either is missing, so a null
// here means the blit is against the default framebuffer's record, which the overload
// declines on IsDefault.
if (FramebufferRecordArmIsMandatory()) {
const auto* readRecord =
BlitEndpointRecord(MG_Pipe::kMGPipeNullHandle, FramebufferTarget::Read);
const auto* drawRecord =
BlitEndpointRecord(MG_Pipe::kMGPipeNullHandle, FramebufferTarget::Draw);
if (readRecord != nullptr && drawRecord != nullptr) {
mask &= ~BlitLayeredDestinationAspects(*readRecord, *drawRecord, srcX0, srcY0, srcX1,
srcY1, dstX0, dstY0, dstX1, dstY1, mask);
}
} else
#endif
mask &= ~BlitLayeredDestinationAspects(
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Read).GetBoundObject(),
MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(), srcX0, srcY0,
@@ -9152,6 +9428,81 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
};
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, CONTRACT-P5E.md §5.4): THE REVERSE INDEX REPLACES THE WALK. id's note
// above names this package as the one that does it, and the reason is the line it
// points at: StateForHandle handed the server a frontend FramebufferObject per live
// twin, which is a SharedPtr to client memory held across records - the last one in
// this family. The record already says which points hold which texture, so the
// question "which framebuffers have this one attached" is answered by an index
// maintained where the record is applied, and the points are re-read from the
// record rather than from the frontend attachment array.
//
// THE SITE ITSELF STAYS INSIDE THE FRONTEND-KEYED SCOPE (§4.4 keeps the class
// exactly here, at DirectGLES.cpp:8837 in the contract's list): every caller of
// this scope reaches it from a BARRIERED row - GenerateMipmap and the two CopyTex
// endpoints - so HandleOf on the texture the caller was handed is legal, and it is
// P8/P9 that retires it. What P5e removes is the per-FRAMEBUFFER frontend hold,
// which is not the caller's own object and has no such excuse.
if (FramebufferRecordArmIsMandatory()) {
const MG_Pipe::MGPipeHandle textureHandle =
TextureImpl::g_backendTextureObjects.HandleOf(texture.get());
if (MG_Pipe::MGPipeHandleIsNull(textureHandle)) return;
const auto detachPoint = [&](FramebufferImpl::BackendFramebufferObject& backendFBO,
FramebufferAttachmentType frontendType,
const MG_Pipe::MGPSurface& surface) {
if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return;
if (!(surface.Res == textureHandle)) return;
GLenum backendAttachment = GL_NONE;
if (frontendType >= FramebufferAttachmentType::Color0 &&
frontendType <= FramebufferAttachmentType::Color31) {
backendAttachment = backendFBO.GetBackendAttachmentType(frontendType);
} else {
backendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType);
}
if (backendAttachment == GL_NONE || backendAttachment == GL_UNKNOWN_MGL) return;
GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(
static_cast<TextureUploadTarget>(surface.UploadTarget));
if (textureTarget == GL_UNKNOWN_MGL) {
textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(
static_cast<TextureTarget>(surface.TextureTarget));
}
const GLuint backendFBOId = backendFBO.GetBackendFramebufferId();
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId);
if (surface.Layered != 0) {
g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, backendAttachment, 0, 0);
} else {
g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, backendAttachment,
textureTarget, 0, 0);
}
ClearGLErrors();
m_detachedAttachments.push_back({backendFBOId, backendAttachment, textureTarget,
backendTextureId, static_cast<GLint>(surface.Level),
surface.Layered != 0});
};
for (const MG_Pipe::MGPipeHandle fbo :
FramebufferImpl::FramebuffersAttachingTexture(textureHandle)) {
auto* twinSlot = FramebufferImpl::g_backendFramebufferObjects.FindByHandle(fbo);
if (twinSlot == nullptr || !*twinSlot) continue; // a recycled slot answers null
const auto* record = FramebufferImpl::PushedFramebufferRecord(fbo);
if (record == nullptr || record->IsDefault != 0) continue;
auto& backendFBO = **twinSlot;
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
detachPoint(backendFBO,
static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) +
static_cast<Int>(i)),
record->Color[i]);
}
detachPoint(backendFBO, FramebufferAttachmentType::Depth, record->Depth);
detachPoint(backendFBO, FramebufferAttachmentType::Stencil, record->Stencil);
}
return;
}
#endif
// Already inside #if MOBILEGL_PIPE_PUSH, so no second guard here: the arm choice
// below is the RUNTIME one.
if (EsprytSlotTablesEnabled()) {
@@ -9946,9 +10297,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
if (!renderbufferObject) return nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution (and the mint on a first
// sync), named debt inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
// P5e (fb, CONTRACT-P5E.md §4.4): the scope at this site is DELETED, and with it the
// arm that needed it. The only caller is the glCopyImageSubData endpoint builder, and a
// RENDERBUFFER endpoint is refused on the wire before it is emitted and again at the
// sink (PipeApplier.cpp's ServerUnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER")),
// so under a transport nothing can reach here with one. Saying so by name is better
// than a find-or-MINT against the frontend address that the refusal already proved
// unreachable - if it ever becomes reachable, the name is where to start.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"CopyImageSubData+RENDERBUFFER\"} a renderbuffer "
"endpoint reached the backend under an active transport, where both the client "
"emitter and the sink refuse one");
std::abort();
}
#endif
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
+2 -2
View File
@@ -9659,8 +9659,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
// (D-C1), which is every point a framebuffer can hold on the handle arm: D-C3 refuses
// bit 9 outright on a driver reporting more than 8 colour attachments, and the
// FRONT/BACK points belong to the DEFAULT framebuffer, which has no twin at all.
static const MG_Pipe::MGPSurface* PushedSurfaceForAttachment(const MG_Pipe::MGPFramebufferState& record,
FramebufferAttachmentType point) {
const MG_Pipe::MGPSurface* PushedSurfaceForAttachment(const MG_Pipe::MGPFramebufferState& record,
FramebufferAttachmentType point) {
if (point == FramebufferAttachmentType::Depth) return &record.Depth;
if (point == FramebufferAttachmentType::Stencil) return &record.Stencil;
if (point < FramebufferAttachmentType::Color0 || point > FramebufferAttachmentType::Color31) {
+11
View File
@@ -2090,6 +2090,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
void NoteFramebufferTextureAttachments(MG_Pipe::MGPipeHandle fbo,
const MG_Pipe::MGPFramebufferState& record);
Vector<MG_Pipe::MGPipeHandle> FramebuffersAttachingTexture(MG_Pipe::MGPipeHandle texture);
// The record's surface for an attachment POINT, or null when this record does not
// describe that point at all (Color8..Color31, and the default framebuffer's FRONT/BACK
// tokens). MGPFramebufferState carries Color[8] + Depth + Stencil, which is every point
// a framebuffer can hold on the handle arm - D-C3 refuses bit 9 outright on a driver
// reporting more than 8 colour attachments. P5e (fb) exports it: the twin's attachment
// walk, the named blit's aspect plan and the detach walk all ask the same question of
// the same record, and a second copy of the Color/Depth/Stencil dispatch beside each of
// them is how one of them ends up describing a point differently from the others.
const MG_Pipe::MGPSurface* PushedSurfaceForAttachment(const MG_Pipe::MGPFramebufferState& record,
FramebufferAttachmentType point);
#endif
// True when the read buffer names a fixed-point (norm/snorm) attachment that the
// backend actually stores in a floating-point format. GL clamps a read from a