[MG_Backend] (Disaggregated): P5e fb - the attachment surface, the two by-handle twins and the image unit read records

- SyncAttachmentSurface loses its frontend attachment parameter (CONTRACT-P5E §5.4).
  The storage syncs have by-handle forms now, so the object was only there to be
  cross-checked - and a second answer only a monolith build can produce is not a
  check. Review N-6's one-directional hole ("record empty, frontend still holding")
  becomes unrepresentable: the caller's detach and the attach read one test.
- The renderbuffer cross-check at the old Managers.cpp:9711 is DELETED rather than
  transport-gated (§4.4). It was the one HandleOf in this family that was not gated
  while its texture sibling was; it ran a client-allocator probe on the apply thread
  under split, legal only because the scope named it and the client was parked.
- BackendFramebufferObject::SyncToBackendByHandle and
  BackendRenderbufferObject::SyncToBackendByHandle land in place of c0e's aborting
  seams, beside the object forms they overload. The record's eleven surfaces are the
  point set; the per-point frontend version memo is gone and the record hash is the
  only gate, with g_attachmentBackendIdGeneration and the re-entry kept because no
  client-side value can answer what they answer.
- SyncReadBufferToBackend's decision moves into ApplyReadBufferFromRecord, which takes
  no frontend object; both overloads funnel through it.
- The reverse index texture -> framebuffers, maintained at the by-handle sync, for the
  detach walk (tx2 needs it) instead of a walk over every twin's frontend attachments.
- Images: ResolveShaderImageRecord and its identity probe are gone - under a transport
  there is no second answer to arbitrate. SyncImageTextureBinding gains a record
  overload; Access is DECODED through MGPipeValueTypes.h (ruling 16 / ID-94) and the
  stale "the encoding does not exist" comment is deleted. The bind's format rules are
  factored into one helper both arms call rather than written twice.
- The image sweep gate is re-keyed onto (ShaderImagesSerial, TextureShutterSerial,
  ContextSerial, g_backendContextGeneration) under a transport (ruling 7 / ID-86),
  never a backend re-mint counter; the window/high-water union is kept and the walk
  reads BoundShaderImages[].
- MarkWritableImageBufferTexturesGpuWritten returns early under a transport - the
  client owns the GPU-write set; paired with sb's deletion of the backend marks.
- BoundImageUnitFormat answers from MGPImageView::InternalFormat under a transport.
This commit is contained in:
2026-09-18 07:53:05 -04:00
parent f6cfcbd36b
commit 3012d52e2b
3 changed files with 950 additions and 321 deletions
+417 -170
View File
@@ -239,6 +239,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& st = MG_Pipe::MGPipeApplier();
return target == FramebufferTarget::Draw ? st.DrawFramebuffer() : st.ReadFramebuffer();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, CONTRACT-P5E.md §5.4 and §0's rule F). THE DECLINE BECOMES THE DETECTOR.
//
// Every framebuffer decline in this file used to fall back to the pre-handle arm, which
// reads GetFramebufferBindingSlot - a BARRIER_PULLED row (FieldOwnership.def). Under an
// active transport that fallback is not a safe default any more: the value it would read
// belongs to a client that is no longer parked behind this apply, so the honest answer is
// the same one the accessor itself would give one line later, raised HERE, where the
// reason is still in scope. Naming the verb is what makes the strict lane's marker table
// ("<field>@<verb>") point at the site instead of at the accessor.
//
// ONLY UNDER A TRANSPORT AND ONLY WITH THE FAMILY BIT SET (ruling 1 / §5.8): the
// push-monolith build keeps its frontend arms token for token, and a mask that never
// armed framebuffers has no record to decline from.
static Bool FramebufferRecordArmIsMandatory() {
return MG_Config::Transport != MG_Config::TransportMode::Monolith && FramebufferSubsystemEnabled();
}
[[noreturn]] static void RefuseFramebufferBindingSlotRead() {
MG_Pipe::MGPipeInputPoisonFatalForVerb(MG_Pipe::MGPipeInputField::GetFramebufferBindingSlot,
MGB_CTX->CurrentVerb());
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
#endif // MOBILEGL_PIPE_PUSH
static Bool IsDualSourceBlendFactor(BlendFactor v) {
@@ -2145,6 +2168,31 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Uint64 g_unitTextureSyncListEpoch = 0;
static Uint64 g_unitTextureSyncListSamplingGeneration = 0;
#if MOBILEGL_PIPE_PUSH
// P5e (fb, CONTRACT-P5E.md §5.4): THE TWO ATTACHMENT LISTS GET AN ENTRY OF THEIR OWN,
// and it borrows nothing.
//
// The unit list's entry borrows the binding slot's SharedPtr and re-checks the pairing
// before every replay, because its key (the unit-bindings epoch) is derived state that
// a silent slot swap could get past. An attachment list under the record arm has
// neither problem and cannot afford the borrow: the key IS the record - {Fbo,
// ContentHash} - and every attachment edit moves the hash by construction, so the
// pairing has nothing left to catch; and the borrowed pointer was into a frontend
// FramebufferObject, which is exactly the thing this package stops holding. So an entry
// is the texture HANDLE the record named plus the twin it resolved to, PairingsIntact
// becomes `entry.Res == surface.Res` folded into the rebuild, and the list owns no
// reference to anything the client can free.
//
// The twin pointer is still a BORROW, on the unit list's own terms: the registry keeps
// a twin alive until its object dies, an attached texture cannot die while the record
// that names it stands, and the very next thing done with the entry is a by-handle sync
// that would re-resolve it anyway.
struct FboAttachmentSyncEntry {
MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle;
BackendTextureObject* backend = nullptr;
};
#endif
// Sibling memo for the draw FBO's texture attachments (see the use site in
// SyncNeccessaryTextures for the key derivation and the borrow rules, which are the
// unit list's). A null FBO pointer means "not stamped".
@@ -2169,6 +2217,58 @@ namespace MobileGL::MG_Backend::DirectGLES {
// a live one.
static Uint64 g_fboTextureSyncListContentHash = 0;
static Bool g_fboTextureSyncListRecordKeyed = false;
// P5e (fb): the record-keyed list itself, held apart from the pre-handle one for the
// reason the two key shapes are held apart - one field carrying two shapes is how a
// stale half gets compared against a live one. Its key is
// {Fbo, ContentHash, ContextSerial, g_backendContextGeneration}: the applier's
// ContextSerial replaces GetTextureContextId (a frontend read the record arm may not
// take), and the two version halves are gone because the hash covers them.
static Vector<FboAttachmentSyncEntry> g_fboAttachmentSyncList;
static MG_Pipe::MGPipeHandle g_fboAttachmentSyncListFbo = MG_Pipe::kMGPipeNullHandle;
static Uint64 g_fboAttachmentSyncListContentHash = 0;
static Uint64 g_fboAttachmentSyncListContextSerial = 0;
static Uint g_fboAttachmentSyncListContextGeneration = 0;
static Bool g_fboAttachmentSyncListValid = false;
// The read framebuffer's, same shape and same reasons. Its own list because it is
// keyed on a different framebuffer and deduped against the draw one.
static Vector<FboAttachmentSyncEntry> g_readFboAttachmentSyncList;
static MG_Pipe::MGPipeHandle g_readFboAttachmentSyncListFbo = MG_Pipe::kMGPipeNullHandle;
static Uint64 g_readFboAttachmentSyncListContentHash = 0;
static Uint64 g_readFboAttachmentSyncListContextSerial = 0;
static Uint g_readFboAttachmentSyncListContextGeneration = 0;
static Bool g_readFboAttachmentSyncListValid = false;
// Rebuild-or-replay, shared by both lists. The record's texture surfaces ARE the
// membership (§5.4); a renderbuffer-only framebuffer - the common Minecraft frame -
// reduces to the key compare and an empty loop.
//
// ONE CALL PER ENTRY, and it is tx2's by-handle seam rather than the three the
// pre-handle arm makes: SyncTextureToBackendByHandle is the by-handle twin of
// SyncTextureObjectToBackend, which is what the REBUILD has always called here, so the
// params / builtin-sampler / mipmap work and its own clean gate are inside it. Calling
// the three separately would need by-handle forms of the first two that nothing
// declares, and would put this file's copy of the clean gate beside tx2's.
static void SyncRecordAttachmentTextures(const MG_Pipe::MGPFramebufferState& record,
Vector<FboAttachmentSyncEntry>& list, Bool listValid) {
if (listValid) {
for (const auto& entry : list) {
TextureImpl::SyncTextureToBackendByHandle(entry.Res, /*imageBindableStorageRequired=*/false);
}
return;
}
list.clear();
const auto add = [&](const MG_Pipe::MGPSurface& surface) {
if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return;
if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return;
auto& twin = TextureImpl::SyncTextureToBackendByHandle(surface.Res,
/*imageBindableStorageRequired=*/false);
list.push_back({surface.Res, twin.get()});
};
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) add(record.Color[i]);
add(record.Depth);
add(record.Stencil);
}
#endif
// The frontend texture-state keys the per-draw texture stages
@@ -2241,6 +2341,74 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Uint64 g_readFboTextureSyncListContentHash = 0;
static Bool g_readFboTextureSyncListRecordKeyed = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, CONTRACT-P5E.md §5.4): THE RECORD ARM OF BOTH ATTACHMENT LISTS, and it is
// the package's core - this is the one unconditional per-draw live read the framebuffer
// family had, taken on every draw whether the memo hit or missed.
//
// Three frontend reads go in one move: the binding slot (twice), the FramebufferObject
// the entries borrowed their attachment slots from, and GetTextureContextId. The key is
// {Fbo, ContentHash, ContextSerial, g_backendContextGeneration} - the record's own hash
// in place of the slot/object version pair (it covers every surface, and Fbo is inside
// it, so a recycled framebuffer cannot be suppressed against its predecessor), the
// APPLIER's ContextSerial in place of the frontend context id, and the backend context
// generation unchanged because it answers a question about driver names that no
// client-side value can answer.
//
// The two lists are still two, and the dedupe is still "one object on both bindings" -
// asked of the two BOUND HANDLES rather than of two pointers, which is the same
// question with a spelling a recycled slot cannot fool.
static void SyncFramebufferAttachmentTexturesByRecord() {
const auto& st = MG_Pipe::MGPipeApplier();
const MG_Pipe::MGPipeHandle drawHandle =
st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Draw)];
const MG_Pipe::MGPipeHandle readHandle =
st.BoundFramebuffer[SizeT(MG_Pipe::MGPipeFramebufferTarget::Read)];
const MG_Pipe::MGPFramebufferState* const drawRecord =
BoundFramebufferRecord(FramebufferTarget::Draw);
const MG_Pipe::MGPFramebufferState* const readRecord =
BoundFramebufferRecord(FramebufferTarget::Read);
const Uint64 contextSerial = st.ContextSerial;
if (drawRecord == nullptr) {
// Not a decline this function can refuse on: SyncCurrentFBO runs before every
// caller of this one and is where the missing record is named and refused
// (RefuseFramebufferBindingSlotRead). Here it only means "nothing to walk".
g_fboAttachmentSyncListValid = false;
g_fboAttachmentSyncList.clear();
} else {
const Bool listValid = g_fboAttachmentSyncListValid &&
g_fboAttachmentSyncListFbo == drawRecord->Fbo &&
g_fboAttachmentSyncListContentHash == drawRecord->ContentHash &&
g_fboAttachmentSyncListContextSerial == contextSerial &&
g_fboAttachmentSyncListContextGeneration == g_backendContextGeneration;
SyncRecordAttachmentTextures(*drawRecord, g_fboAttachmentSyncList, listValid);
g_fboAttachmentSyncListFbo = drawRecord->Fbo;
g_fboAttachmentSyncListContentHash = drawRecord->ContentHash;
g_fboAttachmentSyncListContextSerial = contextSerial;
g_fboAttachmentSyncListContextGeneration = g_backendContextGeneration;
g_fboAttachmentSyncListValid = true;
}
if (readRecord == nullptr || readHandle == drawHandle) {
g_readFboAttachmentSyncListValid = false;
g_readFboAttachmentSyncList.clear();
return;
}
const Bool readListValid = g_readFboAttachmentSyncListValid &&
g_readFboAttachmentSyncListFbo == readRecord->Fbo &&
g_readFboAttachmentSyncListContentHash == readRecord->ContentHash &&
g_readFboAttachmentSyncListContextSerial == contextSerial &&
g_readFboAttachmentSyncListContextGeneration == g_backendContextGeneration;
SyncRecordAttachmentTextures(*readRecord, g_readFboAttachmentSyncList, readListValid);
g_readFboAttachmentSyncListFbo = readRecord->Fbo;
g_readFboAttachmentSyncListContentHash = readRecord->ContentHash;
g_readFboAttachmentSyncListContextSerial = contextSerial;
g_readFboAttachmentSyncListContextGeneration = g_backendContextGeneration;
g_readFboAttachmentSyncListValid = true;
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
static void SyncReadFramebufferTextureAttachments(const DrawTextureSyncKeys& keys,
const MG_State::GLState::FramebufferObject* drawFbo) {
const auto& readSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Read);
@@ -2402,6 +2570,15 @@ 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
// 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).
if (FramebufferRecordArmIsMandatory()) {
SyncFramebufferAttachmentTexturesByRecord();
return;
}
#endif
const auto& drawSlot = GetFramebufferBindingSlotChecked(FramebufferTarget::Draw);
const auto& currentFBO = drawSlot.GetBoundObject();
if (currentFBO) {
@@ -2545,152 +2722,51 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Almost every program (every Minecraft draw) leaves it at zero, which is what keeps the
// draw-path staleness check below at one integer test.
static Uint g_imageUnitHighWaterMark = 0;
#if MOBILEGL_PIPE_PUSH
// P4a e3 (D-G3). The pushed image-unit record for this unit, or null when there is
// none to read.
// ---- P5e (fb, CONTRACT-P5E.md §5.4 / ruling 16 / ID-94): the image unit's bind ------
//
// THE IDENTITY CHECK IS THE POINT. set_shader_images is emitted at the validate point
// for a draw, while SyncImageTextureBinding is also the EAGER funnel that
// glBindImageTexture itself runs - so at that moment the newest record describes the
// previous draw. Driving the driver from a record that names a different texture than
// the unit holds would bind one texture with another's level, layer and format, which
// is the class of bug the frontend's own ImageTextureBinding exists to make
// impossible. So a record is used only when it names EXACTLY the texture the unit
// holds, and otherwise this returns null and the frontend binding answers - which is
// also what a build whose client half has not landed always gets.
// P4a e3's ResolveShaderImageRecord IS GONE, identity test, I5 seam log and all. It
// existed because there were TWO answers for one unit - the record and the frontend
// binding - and it had to decide which of them the driver was driven from; its identity
// test (`view.Res != HandleOf(boundTexture)`) was itself a client-allocator probe on the
// apply thread. Under a transport there is no second answer left: the record IS the
// unit, so the arm below reads it and nothing corroborates it.
//
// Count is the "has this set ever arrived" test, never the serial: MGPipeApplierReset
// advances the working-state serials whether or not anything was emitted.
// AND ACCESS IS DECODED. The comment this replaced said MGPImageView::Access carried
// "an ENCODING whose definition belongs to the client emitter and does not exist at the
// contract commit". That was STALE at the time and is what ruling 16 / ID-94 corrects:
// MGPipeEncodeImageAccess has folded the three GL names into 0/1/2 since P4a, and c0e
// moved the numbers into MG_Pipe/MGPipeValueTypes.h so one table serves both roles.
// Reading the frontend's GLenum "because it is the same value by construction" was the
// last field of this record the server declined to believe.
// The bind itself, factored out of both arms, because the format rules below are the
// part that is easy to get subtly different in two copies - and a divergence there is
// an out-of-bounds image read on nineteen of twenty-six formats, not a wrong pixel.
// Everything it takes is a VALUE: the arm above it decides where each one came from.
//
// WHY THE SEAM CHECKS ARE ONLY LOUD AT THE VALIDATE POINT (review MAJOR-3). The
// framebuffer seam logs unconditionally because SyncCurrentFBO only ever runs where the
// records are current. This one does not have that property: SyncImageTextureBinding is
// ALSO the eager funnel glBindImageTexture itself runs, and at that moment the newest
// record legitimately describes the PREVIOUS draw, so an unconditional log here would
// fire on every ordinary frame and the grep section 8 mandates would be worthless. The
// latch below is set only around the draw/dispatch sweep, where the set for THIS draw
// has been applied and a mismatch is therefore a mis-keyed emission - the same finding
// against B/C the framebuffer line reports, in the same words, so one grep covers all
// three seams.
// Set around BOTH sweeps in SyncImageTextureBindings - the record-driven one and the
// full pre-handle one - because I2's flip fires precisely when the record arm is NOT
// taken (ShaderImageCount == 0 sends the sweep down the wide path), so a latch that
// covered only the record arm would make that flip permanently unreachable.
static Bool g_imageRecordSeamIsAuthoritative = false;
static const MG_Pipe::MGPImageView* ResolveShaderImageRecord(
Uint unit, const MG_State::GLState::ITextureObject* boundTexture) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the image seam's identity check below resolves the
// bound texture's handle by frontend identity - frontend-keyed twin resolution,
// named debt inside the scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
// P4a decline-site I1: M - the mask says this family is not switched on, and it
// IS D's SamplerSubsystemEnabled() now. Silent, confirmed at the verification
// round - and it is what keeps I2 below quiet on a mask that never armed images.
if (!SamplerSubsystemEnabled()) return nullptr;
const auto& st = MG_Pipe::MGPipeApplier();
// P4a decline-site I2: S - FLIPPED AT THE VERIFICATION ROUND to loud-once, under
// the narrowest condition this site can actually test, which is STRONGER than
// the review's "when the program declares images": the validate-point latch is
// set (so this is the draw/dispatch sweep and not the eager glBindImageTexture
// funnel, where an empty set legitimately precedes the first emission) AND the
// caller has already established that this unit HOLDS an image texture
// (SyncImageTextureBinding returns before this on a null one). A unit carrying an
// image texture at a draw with no set_shader_images ever applied is exactly the
// seam defect the review names, and a program that declares no images never
// reaches here at all.
if (st.ShaderImageCount == 0) {
if (g_imageRecordSeamIsAuthoritative) {
MGLOG_E_ONCE("An image record does not describe the binding it names: image unit "
"%u holds a texture at a draw and no set_shader_images has ever been "
"applied; running the pre-handle image bind.",
static_cast<unsigned>(unit));
}
return nullptr;
}
// P4a decline-site I3: M - a unit outside the received window has no pushed
// answer, so the funnel uses the frontend binding. SILENT, CONFIRMED AT THE
// VERIFICATION ROUND and deliberately not folded into I2's flip: nothing in the
// contract pins the window's membership (A8), so a unit outside it is not
// evidence of a defect the way an EMPTY set is. If A8's measurement ever lets C
// document the membership, this is the site that becomes an assertion. (The
// SWEEP's membership is a different question and is not this window - see
// SyncImageTextureBindings and review MAJOR-1.)
if (unit < st.ShaderImageStart || unit - st.ShaderImageStart >= st.ShaderImageCount) return nullptr;
// P4a decline-site I4: unreachable by PipeApply.h:461-464 (Start + Count above the
// bound is Fatal{ProtocolCorruption} in the applier) - kept as defence, no flip
// taken at the verification round. Unlike F5/F7 it does NOT become an assertion:
// those two are guarded by a latch this file sets and can reason about, while
// this bound belongs to the applier, and an index test that survives into a
// release build is the cheaper half of that division of labour.
if (unit >= st.BoundShaderImages.size()) return nullptr;
const MG_Pipe::MGPImageView& view = st.BoundShaderImages[unit];
// P4a decline-site I5: S - THE IMAGE SEAM, loud at the validate point (MAJOR-3)
// and silent at the eager funnel for the reason above. No further flip taken at
// the verification round; it shares the latch with I2.
if (view.Res != g_backendTextureObjects.HandleOf(boundTexture)) {
if (g_imageRecordSeamIsAuthoritative) {
MGLOG_E_ONCE("An image record does not describe the binding it names "
"(image unit %u); running the pre-handle image bind.",
static_cast<unsigned>(unit));
}
return nullptr;
}
return &view;
}
#endif
void SyncImageTextureBinding(Uint unit) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& imageBinding = MGB_CTX->GetImageTextureBinding(static_cast<Int>(unit));
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
g_imageUnitHighWaterMark = unit + 1;
}
if (!imageBinding.Texture) {
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
return;
}
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
#if MOBILEGL_PIPE_PUSH
// P4a e3: the four well-defined fields of the pushed record are the authority for
// this unit once the record names the same texture the unit holds (see
// ResolveShaderImageRecord). ACCESS IS DELIBERATELY NOT AMONG THEM:
// MGPImageView::Access is a Uint8 and a GL access token is not, so the record
// carries an ENCODING whose definition belongs to the client emitter and does not
// exist at the contract commit. Reading it here before that encoding is written
// down would be inventing it. The frontend value answers until then, and it is the
// same value by construction - the client copies the unit's own binding.
const MG_Pipe::MGPImageView* const record =
ResolveShaderImageRecord(unit, imageBinding.Texture.get());
#define MGB_IMAGE_LAYERED (record ? static_cast<GLboolean>(record->Layered) : imageBinding.Layered)
#define MGB_IMAGE_LAYER (record ? static_cast<GLint>(record->Layer) : imageBinding.Layer)
#define MGB_IMAGE_LEVEL (record ? static_cast<GLint>(record->Level) : imageBinding.Level)
#define MGB_IMAGE_FORMAT (record ? static_cast<GLenum>(record->InternalFormat) : imageBinding.Format)
#else
#define MGB_IMAGE_LAYERED imageBinding.Layered
#define MGB_IMAGE_LAYER imageBinding.Layer
#define MGB_IMAGE_LEVEL imageBinding.Level
#define MGB_IMAGE_FORMAT imageBinding.Format
#endif
const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget());
const GLboolean layered = layerable ? MGB_IMAGE_LAYERED : GL_FALSE;
const GLint layer = layerable ? MGB_IMAGE_LAYER : 0;
// The bind half of the image-format widening. SyncTextureObjectToBackend has just
// allocated this texture's storage in the core carrier of its format (the call above
// is the one that marks it image-bindable), and glBindImageTexture's `format` has to
// name the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on
// Adreno for nineteen of the twenty-six non-core formats and on both Malis for
// twenty-five, and every driver that DOES accept a narrow texture through a wide
// image accepts it silently, reading and writing out of bounds. The frontend's own
// ImageTextureBinding keeps the application's format untouched, so
// GL_IMAGE_BINDING_FORMAT still answers what was passed in.
// COMPILED IN THE PULL BUILD TOO, and that is a deliberate exception to the D-P
// discipline the renderbuffer macros keep (each arm's expression at its original site).
// A macro cannot carry a hundred lines of format reasoning, and the alternative was the
// same hundred lines twice; the function is `static` with ONE caller in the pull build,
// so it has no symbol of its own there and the code it generates is the code that was
// written inline before. Named here rather than discovered by a G1 diff.
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
static void IssueImageTextureBind(Uint unit, BackendTextureObject& backendTexture,
TextureTarget textureTarget, TextureInternalFormat textureFormat,
GLenum appFormat, GLint level, Bool layeredRequested,
GLint layerRequested, GLenum access) {
const Bool layerable = SupportsLayeredImageBinding(textureTarget);
const GLboolean layered = layerable ? static_cast<GLboolean>(layeredRequested) : GL_FALSE;
const GLint layer = layerable ? layerRequested : 0;
// The bind half of the image-format widening. The storage sync has just allocated
// this texture's storage in the core carrier of its format (the call above is the
// one that marks it image-bindable), and glBindImageTexture's `format` has to name
// the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on Adreno
// for nineteen of the twenty-six non-core formats and on both Malis for twenty-five,
// and every driver that DOES accept a narrow texture through a wide image accepts it
// silently, reading and writing out of bounds. The frontend's own ImageTextureBinding
// keeps the application's format untouched, so GL_IMAGE_BINDING_FORMAT still answers
// what was passed in.
//
// Widened from the format the APPLICATION named rather than from the texture's own,
// because GL lets the two differ inside one format class and the shader was widened
@@ -2718,34 +2794,132 @@ namespace MobileGL::MG_Backend::DirectGLES {
// format it asked for so that a samplerBuffer reading the same buffer texture - which
// is NOT subscript-rewritten - still sees whole texels. See
// BackendTextureObject::m_bufferImageSplitViewId.
GLenum bindFormat = MGB_IMAGE_FORMAT;
GLuint bindTextureId = backendTexture->GetBackendTextureId();
if (imageBinding.Texture->GetTarget() == TextureTarget::TextureBuffer) {
if (TextureImpl::GetImageBindableBufferSplitFormat(imageBinding.Texture->GetFormat()) !=
GL_UNKNOWN_MGL) {
GLenum bindFormat = appFormat;
GLuint bindTextureId = backendTexture.GetBackendTextureId();
if (textureTarget == TextureTarget::TextureBuffer) {
if (TextureImpl::GetImageBindableBufferSplitFormat(textureFormat) != GL_UNKNOWN_MGL) {
if (const GLenum boundFormatSplit = TextureImpl::GetImageBindableBufferSplitFormat(
MG_Util::ConvertGLEnumToTextureInternalFormat(MGB_IMAGE_FORMAT));
MG_Util::ConvertGLEnumToTextureInternalFormat(appFormat));
boundFormatSplit != GL_UNKNOWN_MGL) {
bindFormat = boundFormatSplit;
if (const Uint splitViewId = backendTexture->GetBufferImageSplitViewId();
if (const Uint splitViewId = backendTexture.GetBufferImageSplitViewId();
splitViewId != 0) {
bindTextureId = splitViewId;
}
}
}
} else if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) {
} else if (TextureImpl::GetImageBindableStorageWidening(textureFormat)) {
const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening(
MG_Util::ConvertGLEnumToTextureInternalFormat(MGB_IMAGE_FORMAT));
MG_Util::ConvertGLEnumToTextureInternalFormat(appFormat));
if (boundFormatWidening) {
bindFormat = boundFormatWidening.InternalFormat;
}
}
g_GLESFuncs.glBindImageTexture(unit, bindTextureId, MGB_IMAGE_LEVEL,
layered, layer, imageBinding.Access, bindFormat);
#undef MGB_IMAGE_LAYERED
#undef MGB_IMAGE_LAYER
#undef MGB_IMAGE_LEVEL
#undef MGB_IMAGE_FORMAT
g_GLESFuncs.glBindImageTexture(unit, bindTextureId, level, layered, layer, access, bindFormat);
}
#if MOBILEGL_PIPE_PUSH
// MGPImageView::Access -> the GL token glBindImageTexture takes. The three numbers are
// MGPipeValueTypes.h's, so this is a spelling change and not a second table; anything
// else on the wire is Fatal{ProtocolCorruption, "ImageView.Access"} AT THE READER,
// which is what the value header's comment asks for (it may not log, so it answers the
// question and the caller owns the refusal).
static GLenum GLAccessForImageView(const MG_Pipe::MGPImageView& view) {
if (!MGPipeImageAccessIsValid(view.Access)) {
MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"ImageView.Access\"} An image record "
"carries Access=%u at unit %u, and only 0/1/2 are defined.",
static_cast<unsigned>(view.Access), static_cast<unsigned>(view.Unit));
std::abort();
}
switch (MGPipeDecodeImageAccess(view.Access)) {
case MGPipeImageAccess::WriteOnly: return GL_WRITE_ONLY;
case MGPipeImageAccess::ReadWrite: return GL_READ_WRITE;
case MGPipeImageAccess::ReadOnly:
case MGPipeImageAccess::Count: break;
}
return GL_READ_ONLY;
}
// A writable buffer image, said by the record: the access byte and the resource
// descriptor's storage kind, neither of which is a frontend read. Same question
// IsWritableImageBufferTexture asks of the frontend binding.
static Bool IsWritableImageBufferView(const MG_Pipe::MGPImageView& view) {
if (MG_Pipe::MGPipeHandleIsNull(view.Res)) return false;
if (!MGPipeImageAccessIsValid(view.Access)) return false;
if (!MGPipeImageAccessWrites(MGPipeDecodeImageAccess(view.Access))) return false;
const auto* record = PipeTextureRecordForHandle(view.Res);
return record != nullptr &&
static_cast<TextureStorageType>(record->Desc.StorageKind) == TextureStorageType::Buffer;
}
// The record form. `Res` is the texture, the four well-defined fields are the record's,
// Access is decoded, and the texture's own TARGET and STORAGE FORMAT - the two
// properties the bind rules need and MGPImageView has no room for (24 bytes, no pad) -
// come off the RESOURCE DESCRIPTOR, which is where the server already keeps them.
void SyncImageTextureBinding(const MG_Pipe::MGPImageView& view) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const Uint unit = static_cast<Uint>(view.Unit);
if (unit >= g_writableImageBufferUnits.size()) return;
TrackWritableImageBufferUnit(unit, IsWritableImageBufferView(view));
if (MG_Pipe::MGPipeHandleIsNull(view.Res)) {
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
return;
}
if (unit + 1 > g_imageUnitHighWaterMark) {
g_imageUnitHighWaterMark = unit + 1;
}
const auto* resource = PipeTextureRecordForHandle(view.Res);
if (resource == nullptr) {
// A record naming a texture the applier has no descriptor for is a seam defect,
// not a binding: there is nothing to allocate image-bindable storage from and
// nothing to widen against. Unbind rather than leave the unit on whatever the
// previous draw put there.
MGLOG_E_ONCE("MGPipe: image unit %u names texture {%u, %u}, which has no applier "
"resource record; unbinding the unit",
static_cast<unsigned>(unit), view.Res.Slot, view.Res.Gen);
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
return;
}
const auto textureFormat = static_cast<TextureInternalFormat>(resource->Desc.InternalFormat);
const TextureTarget textureTarget = PipeTextureTargetForHandle(view.Res);
auto& backendTexture = SyncTextureToBackendByHandle(view.Res, /*imageBindableStorageRequired=*/true);
if (!backendTexture) return;
IssueImageTextureBind(unit, *backendTexture, textureTarget, textureFormat,
static_cast<GLenum>(view.InternalFormat), static_cast<GLint>(view.Level),
view.Layered != 0, static_cast<GLint>(view.Layer),
GLAccessForImageView(view));
}
#endif // MOBILEGL_PIPE_PUSH
// The pre-handle form: the MONOLITH one, and now purely frontend. P4a e3 read the four
// well-defined fields off the record HERE whenever ResolveShaderImageRecord could
// corroborate the identity; P5e takes that whole apparatus out (§5.4) and gives the
// record its OWN overload above, which is the arm a transport selects (ruling 1). What
// is left is what the pull build compiles and what a push-monolith build runs, and the
// values it binds are the same ones either way - the client copies the unit's own
// binding, which is the argument P4a's own comment made for reading Access here.
void SyncImageTextureBinding(Uint unit) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
auto& imageBinding = MGB_CTX->GetImageTextureBinding(static_cast<Int>(unit));
TrackWritableImageBufferUnit(unit, IsWritableImageBufferTexture(imageBinding));
if (imageBinding.Texture && unit + 1 > g_imageUnitHighWaterMark) {
g_imageUnitHighWaterMark = unit + 1;
}
if (!imageBinding.Texture) {
g_GLESFuncs.glBindImageTexture(unit, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
return;
}
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
if (!backendTexture) return;
IssueImageTextureBind(unit, *backendTexture, imageBinding.Texture->GetTarget(),
imageBinding.Texture->GetFormat(), imageBinding.Format,
static_cast<GLint>(imageBinding.Level), imageBinding.Layered != 0,
static_cast<GLint>(imageBinding.Layer), imageBinding.Access);
}
// A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to
@@ -2760,7 +2934,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
// glBindImageTexture performs: that one runs before any shader has touched the buffer,
// and flagging there would pull the driver's copy over a shadow the application may
// still be writing into.
//
// P5e (fb, CONTRACT-P5E.md §5.4): DELETED UNDER A TRANSPORT, and it is one edit paired
// with sb's deletion of the backend's storage-block GPU-write marks. The two were one
// mechanism: the backend told the frontend's buffer shadow "a shader wrote through
// this" because only the backend knew which bindings a dispatch could reach. Under a
// split the CLIENT owns that set and keeps it itself (MG_Impl/Pipe/GpuWritePending.h),
// so the mark has an owner on the side that reads it - and this body cannot run there
// anyway: every line of it is a frontend read (the image binding, the downcast to
// TextureObjectBuffer, the buffer binding slot) plus a HandleOfBuffer probe behind
// MarkBufferGpuWritten. The gate is at the top rather than at the two call sites so
// there is one statement of it, and the unit-tracking bookkeeping below stays out of
// reach of an apply thread entirely.
void MarkWritableImageBufferTexturesGpuWritten() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
#endif
if (g_writableImageBufferUnitCount == 0) return;
for (Uint unit = 0; unit < g_writableImageBufferUnits.size(); ++unit) {
if (!g_writableImageBufferUnits[unit]) continue;
@@ -2837,33 +3026,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (SamplerSubsystemEnabled() && st.ShaderImageCount != 0) {
// P4a decline-site I7: the window/mark UNION (MAJOR-1, fixed here); no flip
// remains at the verification round, only the A8 measurement above.
//
// P5e (fb) KEEPS THE UNION and says so again, because ruling 7 / ID-86 make
// it contract text now (§5.4: "never narrowed to the window"). Under a
// transport the walk additionally READS the applier's array rather than the
// frontend binding at each unit - a unit inside the mark but outside the
// window keeps whatever MGPImageView the last set that covered it left
// there (D-J2: entries outside the window are not cleared), which is the
// record of the very binding that raised the mark.
const Uint32 end = std::min<Uint32>(
std::max<Uint32>(st.ShaderImageStart + st.ShaderImageCount,
static_cast<Uint32>(g_imageUnitHighWaterMark)),
static_cast<Uint32>(unitCount));
// This IS the validate point - the only two callers are the draw gate and
// PrepareForCompute - so the image seam is authoritative for the length of
// the walk and only for that length.
g_imageRecordSeamIsAuthoritative = true;
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
for (Uint32 unit = 0; unit < end; ++unit) {
if (unit >= st.BoundShaderImages.size()) break;
SyncImageTextureBinding(st.BoundShaderImages[unit]);
}
return;
}
#endif
for (Uint32 unit = 0; unit < end; ++unit) {
SyncImageTextureBinding(unit);
}
g_imageRecordSeamIsAuthoritative = false;
return;
}
}
#endif
#if MOBILEGL_PIPE_PUSH
// The validate point either way: the only two callers are the draw gate and
// PrepareForCompute. See I2.
g_imageRecordSeamIsAuthoritative = true;
#if MOBILEGL_BUILD_DISAGGREGATED
// The wide pre-handle sweep reads the frontend binding of every unit, which an
// apply thread may not do. Under a transport there is nothing for it to do anyway:
// g_imageUnitHighWaterMark is raised only by the funnel above, so if no set has
// ever arrived no unit has ever been given an image, and re-binding 0 on units that
// never held one is the no-op the comment above already proved.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
#endif
for (Uint unit = 0; unit < unitCount; ++unit) {
SyncImageTextureBinding(unit);
}
#if MOBILEGL_PIPE_PUSH
g_imageRecordSeamIsAuthoritative = false;
#endif
}
// What the draw path last swept the image units against. A draw never swept them at all:
@@ -2878,6 +3079,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
static Uint64 g_imageSweepSamplingGeneration = 0;
static Uint g_imageSweepBackendContextGeneration = 0;
static Bool g_imageSweepValid = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, ruling 7 / ID-86): the record arm's three applier serials. Held in their own
// fields beside the frontend pair rather than reusing them, for the reason the two
// framebuffer list keys are held apart - one field carrying two key shapes is how a
// stale half gets compared against a live one.
static Uint64 g_imageSweepShaderImagesSerial = 0;
static Uint64 g_imageSweepTextureShutterSerial = 0;
static Uint64 g_imageSweepContextSerial = 0;
#endif
// The sweep is a glBindImageTexture per unit, so it must not run per draw: the gate is the
// frontend's sampling-resolution generation, which TextureObjectBase::BumpShapeVersion
@@ -2899,8 +3109,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
// client's own NewShaderImages shutter mixes the same three frontend counters
// (texture content, texture params, the program's image-unit version), so the
// property is preserved on both sides by construction rather than by agreement.
//
// P5e (fb, ruling 7 / ID-86): PROPERTY 1 IS UNTOUCHED. PROPERTY 2 IS RE-KEYED, and the
// argument behind it is kept rather than dropped - it is what decides WHICH four values
// the new key may be made of.
//
// The frontend sampling-resolution generation cannot be read on an apply thread, so the
// key becomes (ShaderImagesSerial, TextureShutterSerial, ContextSerial,
// g_backendContextGeneration). Three of those are APPLIER-DERIVED - they move when the
// client's own shutters send a new set of images, new texture content or new texture
// parameters, i.e. at exactly the moments the frontend counter moved - and the fourth
// is the backend's ES-context generation, which the old key already carried.
//
// NONE OF THEM IS A BACKEND RE-MINT COUNTER, and that is the whole of the ruling: the
// hazard the old comment names is a texture bound ONLY to an image unit being re-minted
// INSIDE this sweep, which would bump a backend-side epoch after the gate had already
// declined to run. An applier serial moves when the CLIENT said something, strictly
// before the sweep the saying provoked, so the property survives the re-key intact.
// g_backendContextGeneration is not a counter of that kind either - it moves when the
// ES context is rebuilt, which no sweep does.
void SyncImageTextureBindingsForDraw(const DrawTextureSyncKeys& keys) {
if (g_imageUnitHighWaterMark == 0) return;
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto& st = MG_Pipe::MGPipeApplier();
if (g_imageSweepValid && g_imageSweepShaderImagesSerial == st.ShaderImagesSerial &&
g_imageSweepTextureShutterSerial == st.TextureShutterSerial &&
g_imageSweepContextSerial == st.ContextSerial &&
g_imageSweepBackendContextGeneration == g_backendContextGeneration) {
return;
}
SyncImageTextureBindings();
g_imageSweepShaderImagesSerial = st.ShaderImagesSerial;
g_imageSweepTextureShutterSerial = st.TextureShutterSerial;
g_imageSweepContextSerial = st.ContextSerial;
g_imageSweepBackendContextGeneration = g_backendContextGeneration;
g_imageSweepValid = true;
return;
}
#endif
if (g_imageSweepValid && g_imageSweepContextId == keys.contextId &&
g_imageSweepSamplingGeneration == keys.samplingGeneration &&
g_imageSweepBackendContextGeneration == g_backendContextGeneration) {
+497 -151
View File
@@ -4376,6 +4376,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
return PipeRecordAt(MG_Pipe::MGPipeApplier().RenderbufferResources, res);
}
MobileGL::TextureTarget PipeTextureTargetForHandle(MG_Pipe::MGPipeHandle res) {
const auto* record = PipeTextureRecordForHandle(res);
if (record == nullptr) return MobileGL::TextureTarget::Unknown;
// THE INVERSE OF MG_Pipe::MGPipeResourceTargetForTextureTarget, COMPUTED FROM IT rather
// than restated as a second table. MGPipeTypes.h already static_asserts that the
// forward map covers the frontend enum and that it separates the one pair anybody has
// ever folded together (2D vs rectangle), so a linear search over a dozen enumerators
// is total, cannot drift, and runs at most once per image unit per sweep. A second
// switch here is exactly how the two spellings end up disagreeing about a target that
// was added to only one of them.
for (SizeT i = 0; i < static_cast<SizeT>(MobileGL::TextureTarget::TextureTargetCount); ++i) {
const auto target = static_cast<MobileGL::TextureTarget>(i);
if (MG_Pipe::MGPipeResourceTargetForTextureTarget(target) ==
static_cast<Uint32>(record->Desc.Target)) {
return target;
}
}
return MobileGL::TextureTarget::Unknown;
}
const MG_Pipe::MGPipeSamplerCsoRecord* PipeSamplerCsoRecordForHandle(MG_Pipe::MGPipeHandle cso) {
return PipeRecordAt(MG_Pipe::MGPipeApplier().SamplerCsos, cso);
}
@@ -9655,38 +9675,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
// is adopted from MGPSurface::Res through the slot table instead of being looked up by
// the frontend object's ADDRESS, and the attach SHAPE - layered, level, layer, the
// upload target and the texture target - is read off the surface instead of off the
// frontend attachment object. What does NOT move is the storage sync itself: the twins'
// SyncMipmapsToBackend / SyncToBackend still take the frontend object, because that is
// the monolith glue this phase keeps (they read the level shadow for the texels, which
// no record carries), and because those two functions have their OWN handle arms that
// drive the storage from the descriptor.
// frontend attachment object.
//
// The frontend object is therefore still handed over, and it is also CROSS-CHECKED: if
// the record's Res does not name the same twin the frontend attachment does, that is a
// seam defect - two sides that disagree about which texture is attached - and it is
// refused loudly rather than resolved in favour of either.
// P5e (fb, CONTRACT-P5E.md §5.4): THE FRONTEND ATTACHMENT PARAMETER IS GONE. It was
// here for two things and P5e retires both. The storage syncs now have by-handle forms
// (SyncMipmapsToBackendByHandle / SyncToBackendByHandle), so the object is no longer
// needed to reach the texels; and the record is now the ONLY statement of what is
// attached, so the cross-checks had nothing left to corroborate against - a second
// answer that only a monolith build could produce is not a check, it is a second
// writer. With the object gone, review N-6's hole ("record empty, frontend still
// holding") is UNREPRESENTABLE rather than refused: the caller's detach and this
// function's attach now read the SAME test, `Kind == None || Res null`.
//
// The two P5c cross-checks that ran HandleOf on the apply thread go with it. The
// renderbuffer one (the one that was NOT transport-gated, unlike its texture sibling)
// is deleted by the rekey rather than gated, which is what CONTRACT-P5E §4.4 asks for:
// gating it would have left a client-allocator probe compiled into a path a run-ahead
// apply reaches, and there is no longer anything for it to say.
static Bool SyncAttachmentSurface(GLenum glFBOTarget, const MG_Pipe::MGPSurface& surface,
const MG_State::GLState::FramebufferAttachmentObject& attachmentObject,
GLenum glBackendAttachment) {
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
MG_Pipe::MGPipeHandleIsNull(surface.Res)) {
// An empty point. The caller has already detached it where the two sides AGREE
// that the point is empty; attaching nothing is what the legacy arm does here too.
//
// Review N-6: the detach above is decided by the FRONTEND
// (attachmentObject.IsEmpty()) and the attach here by the RECORD, so the one
// direction in which neither fires is "record empty, frontend still holding an
// attachment" - no detach, no attach, and the caller then stamps this point's
// synced version, marking it done with the PREVIOUS owner's image still on the
// driver. Every other record-versus-frontend disagreement in this function is
// loud; this one was the only silent hole and it is closed here rather than
// left one direction wide.
if (!attachmentObject.IsEmpty()) {
MGLOG_E_ONCE("MGPipe: attachment record describes an EMPTY point but the frontend "
"attachment still holds an object - refusing rather than leaving the "
"previous image attached and calling the point synced");
return false;
}
// An empty point: the caller detached it on exactly this test, so attaching
// nothing here is the whole of the agreement. (The legacy arm does the same.)
return true;
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
@@ -9697,31 +9708,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!PipeTwinHandleIsAdoptable(TextureImpl::g_backendTextureObjects, surface.Res, "texture")) {
return false;
}
const auto& textureObject = attachmentObject.GetTexture();
if (!textureObject) {
MGLOG_E_ONCE("MGPipe: attachment record names texture {%u, %u} but the frontend "
"attachment holds no texture - refusing to attach",
surface.Res.Slot, surface.Res.Gen);
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd): the corroborating cross-check against the frontend object's handle
// is a client-allocator probe (T2). Under an active transport the record is the
// ONLY statement of identity (rule E) and the check does not run; monolith
// keeps it verbatim. The record's own handle was already validated above (N-3).
if (MG_Config::Transport == MG_Config::TransportMode::Monolith &&
!(TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()) == surface.Res)) {
#else
if (!(TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()) == surface.Res)) {
#endif
MGLOG_E_ONCE("MGPipe: attachment record names texture {%u, %u} but the frontend "
"attachment's texture %u is handle {%u, %u} - refusing rather than "
"attaching one of them",
surface.Res.Slot, surface.Res.Gen, textureObject->GetExternalIndex(),
TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()).Slot,
TextureImpl::g_backendTextureObjects.HandleOf(textureObject.get()).Gen);
return false;
}
auto* twinSlot = AdoptTwinByHandle(TextureImpl::g_backendTextureObjects, surface.Res, "texture");
if (twinSlot == nullptr) {
return false; // AdoptTwinByHandle named the refusal
@@ -9737,7 +9723,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E_ONCE("%s: No backend texture found for FBO attachment, cannot bind texture.", __func__);
return false;
}
backendTextureObject->SyncMipmapsToBackend(textureObject);
// P5e (fb): the storage sync BY HANDLE (tx2's seam). The level shadow it reads
// is the applier's staged texture store, not a frontend object, which is what
// let the object parameter go.
backendTextureObject->SyncMipmapsToBackendByHandle(surface.Res);
const auto uploadTarget = static_cast<TextureUploadTarget>(surface.UploadTarget);
if (surface.Layered != 0) {
g_GLESFuncs.glFramebufferTexture(glFBOTarget, glBackendAttachment,
@@ -9778,27 +9767,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
"renderbuffer")) {
return false;
}
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
if (!renderbufferObject) {
MGLOG_E_ONCE("MGPipe: attachment record names renderbuffer {%u, %u} but the frontend "
"attachment holds no renderbuffer - refusing to attach",
surface.Res.Slot, surface.Res.Gen);
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (G6, CONTRACT-P5C §5.4): the corroborating cross-check below resolves the
// frontend renderbuffer's handle by frontend identity - named debt inside the
// scope - P3b/P4b rekeys the registry onto handles.
const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry;
#endif
if (!(RenderbufferImpl::g_backendRenderbufferObjects.HandleOf(renderbufferObject.get()) ==
surface.Res)) {
MGLOG_E_ONCE("MGPipe: attachment record names renderbuffer {%u, %u} but the frontend "
"attachment's renderbuffer %u is a different handle - refusing rather "
"than attaching one of them",
surface.Res.Slot, surface.Res.Gen, renderbufferObject->GetExternalIndex());
return false;
}
auto* twinSlot = AdoptTwinByHandle(RenderbufferImpl::g_backendRenderbufferObjects, surface.Res,
"renderbuffer");
if (twinSlot == nullptr) {
@@ -9812,7 +9780,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E_ONCE("%s: No backend renderbuffer found for FBO attachment.", __func__);
return false;
}
backendRenderbufferObject->SyncToBackend(renderbufferObject);
// P5e (fb): the four allocation values were already record-supplied (D-D2); what
// moves here is the LOOKUP - the handle the surface carries, never HandleOf on
// an object the apply thread was handed.
backendRenderbufferObject->SyncToBackendByHandle(surface.Res);
backendRenderbufferObject->Bind();
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
backendRenderbufferObject->GetBackendRenderbufferId());
@@ -9914,6 +9885,89 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#endif
#if MOBILEGL_PIPE_PUSH
// P5e (fb, §5.4): the record half of SyncReadBufferToBackend, lifted out of it so the
// by-handle sync can run it WITHOUT a frontend object. Nothing inside reads one: the
// frontend index was only ever a diagnostic, and it is passed as a number now
// (GlNameForDiag is 0 on the handle arm, which reads as "the record did not say").
void BackendFramebufferObject::ApplyReadBufferFromRecord(const MG_Pipe::MGPFramebufferState& record,
Uint glNameForDiag) {
// D-C1 makes the NULL Res the spelling of "no attachment" and c0c's
// kMGPipeSurfaceKindNone makes Kind agree with it on a zero-initialised record.
// Res stays the test - it is the one D-C1 fixes - and Kind is CROSS-CHECKED
// rather than consulted: the two are one statement the emitter has to make
// twice, and a record where they disagree is a seam defect, not an empty point.
// Picking a winner silently is what this refuses to do.
const Bool readSurfaceEmpty = MG_Pipe::MGPipeHandleIsNull(record.ReadSurface.Res);
if (readSurfaceEmpty != (record.ReadSurface.Kind == MG_Pipe::kMGPipeSurfaceKindNone)) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface says Res={%u, %u} and "
"Kind=%u, which disagree about whether it names anything - refusing "
"rather than choosing one of them",
glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen,
record.ReadSurface.Kind);
return;
}
FramebufferAttachmentType pushedReadBuf = FramebufferAttachmentType::None;
if (!readSurfaceEmpty) {
// EVERY FIELD THAT IDENTIFIES THE IMAGE, not the three v1 compared (m-4).
// The same image legally sits at two colour points - a layered attachment of
// one array and a single slice of it are different surfaces of one Res - so a
// partial comparison resolves to the lower index and the refusal branch below
// ("refusing to guess") would then be doing part of the guessing itself.
// InternalFormat and TextureTarget are properties of the RESOURCE rather than
// of the point, so they add nothing to the identity and are left out.
Bool matched = false;
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) {
pushedReadBuf = static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i));
matched = true;
break;
}
}
if (!matched) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface {%u, %u} matches no "
"colour attachment of its own record - refusing to guess a read "
"buffer",
glNameForDiag, record.ReadSurface.Res.Slot, record.ReadSurface.Res.Gen);
return;
}
}
if (pushedReadBuf == m_frontendReadBuffer) {
return;
}
m_frontendReadBuffer = pushedReadBuf;
const GLenum glPushedReadBuffer = GetBackendAttachmentType(pushedReadBuf);
if (m_backendReadBuffer != glPushedReadBuffer) {
m_backendReadBuffer = glPushedReadBuffer;
// Still bound as READ first, for the reason the legacy arm gives below:
// glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER.
Bind(FramebufferTarget::Read);
g_GLESFuncs.glReadBuffer(glPushedReadBuffer);
}
}
// P5e (fb): the by-handle entry, for SyncCurrentFBOByRecord's "one object on both
// bindings" skip - the one path that applies a read buffer without doing the rest of
// the sync. A framebuffer with no record is the caller's seam defect, not a silent
// no-op, so it says so.
void BackendFramebufferObject::SyncReadBufferToBackendByHandle(MG_Pipe::MGPipeHandle fbo) {
const auto* record = PushedFramebufferRecord(fbo);
if (record == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so its read "
"buffer cannot be pushed",
fbo.Slot, fbo.Gen);
return;
}
ApplyReadBufferFromRecord(*record, 0);
}
#endif // MOBILEGL_PIPE_PUSH
void BackendFramebufferObject::SyncReadBufferToBackend(
const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject) {
if (!stateFBOObject) {
@@ -9933,6 +9987,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// spelling of "no attachment" independently of Kind's numbering) is
// FramebufferAttachmentType::None, i.e. GL_NONE. A surface that matches no colour
// point cannot be a legal read buffer and is refused rather than guessed.
//
// P5e (fb): THAT DECISION now lives in ApplyReadBufferFromRecord, which takes no
// frontend object; what is left in this overload is the monolith glue that finds
// the handle by identity. Under an active transport nothing reaches it -
// SyncToBackendByHandle runs the record form directly.
if (FramebufferSubsystemEnabled()) {
// P5c (hd): with an active transport the handle is the caller's
// (m_pushedSyncHandle), never the client allocator's - see SyncToBackend.
@@ -9955,66 +10014,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
stateFBOObject->GetExternalIndex(), fbo.Slot, fbo.Gen);
return;
}
// D-C1 makes the NULL Res the spelling of "no attachment" and c0c's
// kMGPipeSurfaceKindNone makes Kind agree with it on a zero-initialised record.
// Res stays the test - it is the one D-C1 fixes - and Kind is CROSS-CHECKED
// rather than consulted: the two are one statement the emitter has to make
// twice, and a record where they disagree is a seam defect, not an empty point.
// Picking a winner silently is what this refuses to do.
const Bool readSurfaceEmpty = MG_Pipe::MGPipeHandleIsNull(record->ReadSurface.Res);
if (readSurfaceEmpty != (record->ReadSurface.Kind == MG_Pipe::kMGPipeSurfaceKindNone)) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface says Res={%u, %u} and "
"Kind=%u, which disagree about whether it names anything - refusing "
"rather than choosing one of them",
stateFBOObject->GetExternalIndex(), record->ReadSurface.Res.Slot,
record->ReadSurface.Res.Gen, record->ReadSurface.Kind);
return;
}
FramebufferAttachmentType pushedReadBuf = FramebufferAttachmentType::None;
if (!readSurfaceEmpty) {
// EVERY FIELD THAT IDENTIFIES THE IMAGE, not the three v1 compared (m-4).
// The same image legally sits at two colour points - a layered attachment of
// one array and a single slice of it are different surfaces of one Res - so a
// partial comparison resolves to the lower index and the refusal branch below
// ("refusing to guess") would then be doing part of the guessing itself.
// InternalFormat and TextureTarget are properties of the RESOURCE rather than
// of the point, so they add nothing to the identity and are left out.
Bool matched = false;
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) {
pushedReadBuf = static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i));
matched = true;
break;
}
}
if (!matched) {
MGLOG_E_ONCE("MGPipe: framebuffer %u's resolved read surface {%u, %u} matches no "
"colour attachment of its own record - refusing to guess a read "
"buffer",
stateFBOObject->GetExternalIndex(), record->ReadSurface.Res.Slot,
record->ReadSurface.Res.Gen);
return;
}
}
if (pushedReadBuf == m_frontendReadBuffer) {
return;
}
m_frontendReadBuffer = pushedReadBuf;
const GLenum glPushedReadBuffer = GetBackendAttachmentType(pushedReadBuf);
if (m_backendReadBuffer != glPushedReadBuffer) {
m_backendReadBuffer = glPushedReadBuffer;
// Still bound as READ first, for the reason the legacy arm gives below:
// glReadBuffer targets whatever FBO is bound to GL_READ_FRAMEBUFFER.
Bind(FramebufferTarget::Read);
g_GLESFuncs.glReadBuffer(glPushedReadBuffer);
}
ApplyReadBufferFromRecord(*record, stateFBOObject->GetExternalIndex());
return;
}
#endif
@@ -10390,7 +10390,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
frontendType <= FramebufferAttachmentType::Color31 &&
(static_cast<Int>(frontendType) - static_cast<Int>(FramebufferAttachmentType::Color0)) <
g_GLESCapabilities.MaxColorAttachments;
if (isColorPoint && attachmentObject.IsEmpty() && glBackendAttachment != GL_NONE) {
#if MOBILEGL_PIPE_PUSH
// P5e (fb, §5.4): ON THE RECORD ARM THE EMPTY POINT IS THE RECORD'S, and it
// is the SAME test SyncAttachmentSurface attaches on - `Kind == None ||
// Res null`. Asking the frontend here and the record there is what left
// review N-6's one-directional hole; one test closes it by construction.
const Bool pointIsEmpty =
pushedRecord != nullptr
? [&] {
const MG_Pipe::MGPSurface* s =
PushedSurfaceForAttachment(*pushedRecord, frontendType);
return s == nullptr || s->Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
MG_Pipe::MGPipeHandleIsNull(s->Res);
}()
: attachmentObject.IsEmpty();
#else
const Bool pointIsEmpty = attachmentObject.IsEmpty();
#endif
if (isColorPoint && pointIsEmpty && glBackendAttachment != GL_NONE) {
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER, 0);
}
#if MOBILEGL_PIPE_PUSH
@@ -10418,8 +10435,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
attachmentSynced = false;
}
} else {
attachmentSynced = SyncAttachmentSurface(glFBOTarget, *pushedSurface, attachmentObject,
glBackendAttachment);
attachmentSynced =
SyncAttachmentSurface(glFBOTarget, *pushedSurface, glBackendAttachment);
}
} else {
attachmentSynced = SyncAttachmentObject(glFBOTarget, attachmentObject, glBackendAttachment);
@@ -10511,6 +10528,253 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
#if MOBILEGL_PIPE_PUSH
// ---- P5e (fb, CONTRACT-P5E.md §5.4): the reverse index, texture -> framebuffers ------
//
// WHY IT EXISTS. ScopedDetachedTextureFramebufferAttachments used to answer "which
// framebuffers currently have this texture attached" by walking every live twin and
// reading each one's FRONTEND attachment array. Under P5e that walk is the last place
// the server holds a frontend framebuffer across records - id re-typed ForEachLive and
// left the StateForHandle read greppable, naming this package as the one that replaces
// it. The records already carry the answer: every applied set_framebuffer_state names
// its eleven surfaces, so the relation is maintained where the record is consumed.
//
// KEYED ON THE TEXTURE SLOT rather than on {slot, gen}. The question a detach asks is
// "does the driver have this texture id hanging off some framebuffer", the points are
// re-derived from the record on every hit, and a stale entry left by a recycled slot
// therefore costs one record walk that matches nothing. The FRAMEBUFFER handles are
// stored whole, so a recycled framebuffer slot never answers for its predecessor
// (FindByHandle refuses a moved Gen).
//
// MAINTAINED AT THE SYNC, NOT AT THE APPLY, and that is the same set rather than a
// smaller one: a framebuffer whose record was never synced has no driver attachment to
// detach, and every path that can have one runs this sync first.
namespace {
UnorderedMap<Uint32, Vector<MG_Pipe::MGPipeHandle>> g_framebuffersByAttachedTextureSlot;
UnorderedMap<Uint32, Vector<Uint32>> g_attachedTextureSlotsByFramebufferSlot;
void ForgetFramebufferTextureAttachmentsLocked(Uint32 fboSlot) {
auto it = g_attachedTextureSlotsByFramebufferSlot.find(fboSlot);
if (it == g_attachedTextureSlotsByFramebufferSlot.end()) return;
for (const Uint32 textureSlot : it->second) {
auto listIt = g_framebuffersByAttachedTextureSlot.find(textureSlot);
if (listIt == g_framebuffersByAttachedTextureSlot.end()) continue;
auto& list = listIt->second;
list.erase(std::remove_if(list.begin(), list.end(),
[&](MG_Pipe::MGPipeHandle h) { return h.Slot == fboSlot; }),
list.end());
if (list.empty()) g_framebuffersByAttachedTextureSlot.erase(listIt);
}
g_attachedTextureSlotsByFramebufferSlot.erase(it);
}
} // namespace
void NoteFramebufferTextureAttachments(MG_Pipe::MGPipeHandle fbo,
const MG_Pipe::MGPFramebufferState& record) {
if (MG_Pipe::MGPipeHandleIsNull(fbo)) return;
ForgetFramebufferTextureAttachmentsLocked(fbo.Slot);
Vector<Uint32> textureSlots;
const auto note = [&](const MG_Pipe::MGPSurface& surface) {
if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return;
if (MG_Pipe::MGPipeHandleIsNull(surface.Res)) return;
if (std::find(textureSlots.begin(), textureSlots.end(), surface.Res.Slot) != textureSlots.end()) {
return; // one texture at two points is one entry
}
textureSlots.push_back(surface.Res.Slot);
g_framebuffersByAttachedTextureSlot[surface.Res.Slot].push_back(fbo);
};
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) note(record.Color[i]);
note(record.Depth);
note(record.Stencil);
if (textureSlots.empty()) return; // the common Minecraft frame: renderbuffers only
g_attachedTextureSlotsByFramebufferSlot[fbo.Slot] = std::move(textureSlots);
}
// BY VALUE, and deliberately: the caller re-binds framebuffers while it walks the
// answer, and anything that syncs one re-enters NoteFramebufferTextureAttachments and
// can rehash the map underneath an iterator.
//
// NOTHING PRUNES A DEAD FRAMEBUFFER'S ENTRY and nothing needs to. A stale handle is
// refused by FindByHandle (its Gen has moved), so it contributes nothing; and the
// moment the slot is reused, the successor's first sync clears its predecessor's rows
// here. The index is therefore bounded by the live framebuffer slot count, which is
// the same bound the twin table itself carries.
Vector<MG_Pipe::MGPipeHandle> FramebuffersAttachingTexture(MG_Pipe::MGPipeHandle texture) {
if (MG_Pipe::MGPipeHandleIsNull(texture)) return {};
auto it = g_framebuffersByAttachedTextureSlot.find(texture.Slot);
if (it == g_framebuffersByAttachedTextureSlot.end()) return {};
return it->second;
}
// ---- P5e (fb, §5.4): BackendFramebufferObject::SyncToBackend BY HANDLE ---------------
//
// The record's ELEVEN SURFACES ARE THE POINT SET. The object form walks the frontend's
// 41 attachment points to learn which exist; this one does not have to, because the
// emitter REFUSES a framebuffer holding a point at or above the wire width
// (FramebufferEmit.h's two refusals) and the FRONT/BACK tokens belong to the default
// framebuffer, which has no twin. So Color[0..7] + Depth + Stencil is the whole of it,
// and a framebuffer that could not be described that way never produced a record at all.
//
// THE PER-POINT VERSION MEMO IS GONE and is not replaced. On the object form it is a
// second, narrower gate under the record hash, hedging against "a frontend attachment
// moved without the record moving" - which is unrepresentable once the record is the
// only statement of what is attached (the hash covers every surface, and Fbo is inside
// it, so a recycled handle cannot be suppressed against its predecessor). Keeping it
// would have meant keeping the frontend array that carries it, which is the whole point
// of this arm. The two SERVER-owned gates stay, because no client-side value can answer
// what they answer: g_attachmentBackendIdGeneration ("did I re-mint a driver id") and
// the re-entry it drives.
void BackendFramebufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo,
FramebufferTarget asTarget) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const MG_Pipe::MGPFramebufferState* const recordPtr = PushedFramebufferRecord(fbo);
if (recordPtr == nullptr) {
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} has no applier record, so it is not "
"configured here; nothing is bound by this refusal, but the caller's own "
"bind still targets it",
fbo.Slot, fbo.Gen);
return;
}
const MG_Pipe::MGPFramebufferState& record = *recordPtr;
if (record.IsDefault != 0) {
// The default framebuffer has no twin (pDefaultFramebufferInfo->defaultFBO is the
// frontend object and MGPipeHandles.h reserves {0,1} for it), so reaching this
// body with one is a caller that skipped its own IsDefault branch.
MGLOG_E_ONCE("MGPipe: framebuffer handle {%u, %u} describes the DEFAULT framebuffer, "
"which has no twin to configure",
fbo.Slot, fbo.Gen);
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Kept in step for the monolith-glue overloads that still resolve their record
// through it (SyncReadBufferToBackend's push arm).
m_pushedSyncHandle = fbo;
#endif
const GLenum glFBOTarget = MG_Util::ConvertFramebufferTargetToGLEnum(asTarget);
Bind(asTarget);
FramebufferObject::FramebufferAttachmentArray stateDrawBuffers{};
DecodePushedDrawBuffers(record, stateDrawBuffers.data());
// 1. Remap draw buffers. Same rule as the object form: glDrawBuffers writes the
// DRAW-bound framebuffer's state, so a READ-only sync must neither issue it nor
// stamp the memo (the Minecraft 26.x OIT bug the object form names).
Bool attachmentPointsMoved = false;
const Bool drawBufferClean =
memcmp(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType)) == 0;
if (!drawBufferClean && asTarget == FramebufferTarget::Draw) {
memcpy(m_frontendDrawBuffers, stateDrawBuffers.data(),
FramebufferObject::MAX_DRAW_BUFFERS * sizeof(FramebufferAttachmentType));
std::fill(m_backendDrawBuffers, m_backendDrawBuffers + FramebufferObject::MAX_DRAW_BUFFERS, GL_NONE);
int nEffectiveBuffers = 0;
for (GLint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS; ++i) {
const auto frontendBuf = stateDrawBuffers[i];
if (frontendBuf == FramebufferAttachmentType::None) {
m_backendDrawBuffers[i] = GL_NONE;
continue;
}
// DecodePushedDrawBuffers cannot produce a FRONT/BACK token - those are the
// default framebuffer's own and it has no twin - so the object form's
// "shouldn't remap" branch has no counterpart here.
m_backendDrawBuffers[i] = GL_COLOR_ATTACHMENT0 + i;
nEffectiveBuffers = i + 1;
}
g_GLESFuncs.glDrawBuffers(nEffectiveBuffers, m_backendDrawBuffers);
if (RecomputeBackendColorSlots(stateDrawBuffers)) {
m_frontendReadBuffer = FramebufferAttachmentType::Unknown;
attachmentPointsMoved = true;
}
}
// The four cross-object masks, off the record's own surfaces (ID-12 DV-5).
if (asTarget == FramebufferTarget::Draw) {
Uint32 snormClampOutputMask = 0;
Uint32 unormClampOutputMask = 0;
Uint32 alphaWidenedMask = 0;
Uint32 integerColorMask = 0;
for (Uint i = 0; i < FramebufferObject::MAX_DRAW_BUFFERS && i < 32; ++i) {
const auto frontendBuf = stateDrawBuffers[i];
if (frontendBuf < FramebufferAttachmentType::Color0 ||
frontendBuf > FramebufferAttachmentType::Color31) {
continue;
}
const MG_Pipe::MGPSurface* surface = PushedSurfaceForAttachment(record, frontendBuf);
if (surface == nullptr) continue; // unrepresentable: DecodePushedDrawBuffers' range
if (IsSnormFallbackSurface(*surface)) {
snormClampOutputMask |= (1u << i);
} else if (IsUnormFallbackSurface(*surface)) {
unormClampOutputMask |= (1u << i);
}
if (IsAlphaWidenedColorSurface(*surface)) alphaWidenedMask |= (1u << i);
if (IsIntegerColorSurface(*surface)) integerColorMask |= (1u << i);
}
PrgramImpl::g_snormFallbackClampOutputMask = snormClampOutputMask;
PrgramImpl::g_unormFallbackClampOutputMask = unormClampOutputMask;
g_alphaWidenedDrawBufferMask = alphaWidenedMask;
g_integerColorDrawBufferMask = integerColorMask;
}
// 2. Remap read buffer, READ-target only, for the reason the object form gives.
if (asTarget == FramebufferTarget::Read) {
ApplyReadBufferFromRecord(record, 0);
}
// 3. The attachments. Two gates, both server-owned.
const Bool idGenerationMoved = m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration;
if (idGenerationMoved) {
m_syncedBackendIdGeneration = g_attachmentBackendIdGeneration;
}
if (idGenerationMoved || attachmentPointsMoved ||
m_syncedRecordHashes[SizeT(asTarget)] != record.ContentHash) {
const auto applyPoint = [&](FramebufferAttachmentType point,
const MG_Pipe::MGPSurface& surface) {
const Bool isColorPoint = point >= FramebufferAttachmentType::Color0 &&
point <= FramebufferAttachmentType::Color31;
const GLenum glBackendAttachment =
isColorPoint ? GetBackendAttachmentType(point)
: MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(point);
if (glBackendAttachment == GL_NONE || glBackendAttachment == GL_UNKNOWN_MGL) return;
const Bool empty = surface.Kind == MG_Pipe::kMGPipeSurfaceKindNone ||
MG_Pipe::MGPipeHandleIsNull(surface.Res);
// The detach that keeps m_backendColorSlots a permutation of the PHYSICAL
// layout: a colour point whose record says empty must not keep the previous
// owner's image, or glReadBuffer returns it. Bounded by the driver's cap,
// because GL_COLOR_ATTACHMENTn above it is INVALID_ENUM.
if (isColorPoint && empty) {
const Int index =
static_cast<Int>(point) - static_cast<Int>(FramebufferAttachmentType::Color0);
if (index < g_GLESCapabilities.MaxColorAttachments) {
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment,
GL_RENDERBUFFER, 0);
}
return;
}
SyncAttachmentSurface(glFBOTarget, surface, glBackendAttachment);
};
for (Uint i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
applyPoint(static_cast<FramebufferAttachmentType>(
static_cast<Int>(FramebufferAttachmentType::Color0) + static_cast<Int>(i)),
record.Color[i]);
}
applyPoint(FramebufferAttachmentType::Depth, record.Depth);
applyPoint(FramebufferAttachmentType::Stencil, record.Stencil);
m_syncedRecordHashes[SizeT(asTarget)] = record.ContentHash;
NoteFramebufferTextureAttachments(fbo, record);
}
// The walk itself can re-mint a driver id (the storage sync behind a texture
// surface), invalidating points this walk already attached. Re-enter until the
// generation is quiescent, exactly as the object form does - every pass syncs each
// dirty texture clean, so each repeat finds strictly fewer re-mints.
if (m_syncedBackendIdGeneration != g_attachmentBackendIdGeneration) {
SyncToBackendByHandle(fbo, asTarget);
}
}
#endif // MOBILEGL_PIPE_PUSH
GLenum BackendFramebufferObject::GetBackendAttachmentType(FramebufferAttachmentType frontendAtt) const {
// Only colour attachments are ever relocated; depth/stencil, the default framebuffer's
// FRONT/BACK names and None map straight through.
@@ -10950,6 +11214,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
// outside the frontend's array, which cannot be addressed at all.
Uint BoundImageUnitFormat(Int unit) {
if (unit < 0 || unit >= MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS) return 0;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, §5.4): the format-less image bake asks what format the APPLICATION
// named at this unit, and under a transport the record is the only statement of
// it - MGPImageView::InternalFormat is that same GLenum, copied by the client
// out of the unit's own binding. A unit the applier has never been told about
// answers 0, which is what an unbound unit answers on the frontend arm too.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto& st = MG_Pipe::MGPipeApplier();
if (static_cast<SizeT>(unit) >= st.BoundShaderImages.size()) return 0;
return static_cast<Uint>(st.BoundShaderImages[static_cast<SizeT>(unit)].InternalFormat);
}
#endif
return static_cast<Uint>(MGB_CTX->GetImageTextureBinding(unit).Format);
}
@@ -13798,6 +14074,85 @@ namespace MobileGL::MG_Backend::DirectGLES {
#undef MGB_RBO_HEIGHT
#undef MGB_RBO_SAMPLES
#if MOBILEGL_PIPE_PUSH
// P5e (fb, CONTRACT-P5E.md §5.4): the same allocation, keyed on the renderbuffer HANDLE.
//
// THE FOUR VALUES WERE ALREADY THE RECORD'S at P4a (D-D2) - what the object form still
// needed the frontend for was the LOOKUP, `HandleOf(stateRBOObject.get())`, a
// client-allocator probe on the apply thread. On this arm the handle arrives from the
// attachment surface that named it, so nothing is probed and nothing is dereferenced.
// An OVERLOAD rather than a changed signature (the monolith glue keeps its tokens).
void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) {
#ifdef TRACY_ENABLE
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
const MG_Pipe::MGPipeResourceRecord* const pushedRecord =
PipeRenderbufferRecordForHandle(renderbuffer);
if (pushedRecord == nullptr) {
MGLOG_E_ONCE("MGPipe: renderbuffer handle {%u, %u} has no applier record, so its "
"storage cannot be allocated from the pushed descriptor",
renderbuffer.Slot, renderbuffer.Gen);
return;
}
if (m_isInitialized && m_syncedResourceSerial != 0 &&
m_syncedResourceSerial == pushedRecord->Serial) {
return;
}
// A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3): the
// allocation below re-defines the storage behind the name the framebuffer twins
// already attached, and no record of THEIRS moves for it, so the attachment memo
// has to be re-armed the same way a texture re-mint arms it. The first allocation
// is not a redefinition.
if (m_isInitialized) {
++FramebufferImpl::g_attachmentBackendIdGeneration;
}
Bind();
const auto internalFormat = static_cast<TextureInternalFormat>(pushedRecord->Desc.InternalFormat);
const Int width = static_cast<Int>(pushedRecord->Desc.Width);
const Int height = static_cast<Int>(pushedRecord->Desc.Height);
const Int samples = static_cast<Int>(pushedRecord->Desc.Samples);
GLenum glInternalFormat, glType, glFormat;
TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType);
// Drain first, for the object form's reason: a driver that refuses the allocation
// (a multi-gigabyte renderbuffer is refused routinely) used to leave the twin
// claiming storage it does not have, and the attachment then rendered nowhere.
DebugImpl::ErrorLopper::Clear();
if (samples > 0) {
const auto backendSamples = static_cast<GLsizei>(ClampSamplesToBackendSupport(
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
} else {
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
}
if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) {
MGLOG_E_ONCE("Renderbuffer {%u, %u} storage allocation ran out of memory: %dx%d, "
"samples=%d, format=%s",
renderbuffer.Slot, renderbuffer.Gen, width, height, samples,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
// NO RecordError HERE. The object form reports the OOM to the application
// through the live GLContext; on this arm there is no application on this side
// of the wire to report it to, and MGB_CTX->RecordError is itself one of the
// BARRIER-PULLED rows (FieldOwnership.def) that rule F forbids an unbarriered
// apply to touch. The client raises its own errors from its own validator.
}
DebugImpl::ErrorLopper::Clear();
m_cacheInternalFormat = internalFormat;
m_cacheWidth = width;
m_cacheHeight = height;
m_cacheSamples = samples;
m_syncedResourceSerial = pushedRecord->Serial;
m_isInitialized = true;
}
#endif // MOBILEGL_PIPE_PUSH
TwinRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject, MG_Pipe::MGPipeKind::Renderbuffer>
g_backendRenderbufferObjects;
} // namespace RenderbufferImpl
@@ -13850,13 +14205,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
} // namespace TextureImpl
namespace FramebufferImpl {
void BackendFramebufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle fbo,
FramebufferTarget asTarget) {
(void)asTarget;
MGPipeP5eSeamNotLanded("BackendFramebufferObject::SyncToBackendByHandle", "fb", fbo);
}
} // namespace FramebufferImpl
// FramebufferImpl::BackendFramebufferObject::SyncToBackendByHandle and
// RenderbufferImpl::BackendRenderbufferObject::SyncToBackendByHandle LANDED with package fb
// (P5e); their bodies are beside the object forms they overload, where the attachment walk
// and the storage allocation they share can be read side by side.
namespace PrgramImpl {
void BackendProgramObjectImpl::SyncToBackendByHandle(MG_Pipe::MGPipeHandle cso) {
@@ -13864,11 +14216,5 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
} // namespace PrgramImpl
namespace RenderbufferImpl {
void BackendRenderbufferObject::SyncToBackendByHandle(MG_Pipe::MGPipeHandle renderbuffer) {
MGPipeP5eSeamNotLanded("BackendRenderbufferObject::SyncToBackendByHandle", "fb",
renderbuffer);
}
} // namespace RenderbufferImpl
#endif // MOBILEGL_PIPE_PUSH
} // namespace MobileGL::MG_Backend::DirectGLES
+36
View File
@@ -724,6 +724,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// composite: this reader hides the split behind one lookup, exactly as the wire does.
const MG_Pipe::MGPipeShaderCsoRecord* PipeShaderCsoRecordForHandle(MG_Pipe::MGPipeHandle cso);
// P5e (fb): the texture's own TARGET, from its descriptor. The image-unit bind needs it
// (glBindImageTexture's layered/format rules are per target) and MGPImageView has no room
// for it - 24 bytes, no pad - so it is read off the resource record instead of being added
// to the wire. TextureTarget::Unknown when the handle names no live texture record, which
// every caller treats as "decline", never as a default.
MobileGL::TextureTarget PipeTextureTargetForHandle(MG_Pipe::MGPipeHandle res);
// The pending-upload entry the applier accumulated for this (uploadTarget, level) of this
// texture record, or null (D-D5). SERVER-SIDE STATE, and that is the whole point: the
// client clears its own dirty flags at EMISSION for the levels the applier accepted, while
@@ -1934,6 +1941,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// still run when SyncCurrentFBO skips the READ-target sync because the same GL FBO is
// bound as both draw and read (otherwise glReadBuffer changes would be silently dropped).
void SyncReadBufferToBackend(const SharedPtr<MG_State::GLState::FramebufferObject>& stateFBOObject);
#if MOBILEGL_PIPE_PUSH
// P5e (fb, §5.4): the same read-buffer push keyed on the handle, for the one path
// that applies a read buffer without doing the rest of the sync - SyncCurrentFBO's
// "one object is bound to BOTH bindings" skip, where the DRAW pass already did the
// attachment work and only glReadBuffer is READ-target-specific.
void SyncReadBufferToBackendByHandle(MG_Pipe::MGPipeHandle fbo);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (hd, CONTRACT-P5C §3.2): the framebuffer handle the CURRENT sync is keyed on.
// A caller applying a record sets it before SyncToBackend / SyncReadBufferToBackend,
@@ -2023,6 +2037,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// did. 0 is never a live hash (a computed 0 is remapped to 1 by the client's
// suppressor), so a zeroed memo is a guaranteed miss.
Array<Uint64, SizeT(FramebufferTarget::FramebufferTargetCount)> m_syncedRecordHashes = {0};
// P5e (fb): the read-buffer decision, taken from the record alone. Both
// SyncReadBufferToBackend overloads funnel through it, so the rule lives in one
// place and the object form is visibly the half that only finds the handle.
// glNameForDiag is 0 on the handle arm, which reads as "the record did not say".
void ApplyReadBufferFromRecord(const MG_Pipe::MGPFramebufferState& record, Uint glNameForDiag);
#endif
};
@@ -2054,6 +2073,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
// describes an object without claiming any binding for it, so asking the record would
// give the wrong answer by construction.
Bool PushedFramebufferIsBoundTo(FramebufferTarget target, MG_Pipe::MGPipeHandle fbo);
// ---- P5e (fb, CONTRACT-P5E.md §5.4): the reverse index, texture -> framebuffers ------
//
// "Which framebuffers currently have this texture attached." The detach walk
// (ScopedDetachedTextureFramebufferAttachments) used to answer it by iterating every
// live twin and reading each one's FRONTEND attachment array, which is the last place
// the server held a frontend framebuffer across records - id re-typed ForEachLive and
// named this package as the one that replaces the read. The relation is maintained
// where the record is consumed (SyncToBackendByHandle's attachment walk), because the
// record's eleven surfaces already say it.
//
// The index answers about a TEXTURE SLOT and returns whole FRAMEBUFFER handles, so a
// recycled framebuffer slot never answers for its predecessor. See the definition for
// why nothing prunes a dead row.
void NoteFramebufferTextureAttachments(MG_Pipe::MGPipeHandle fbo,
const MG_Pipe::MGPFramebufferState& record);
Vector<MG_Pipe::MGPipeHandle> FramebuffersAttachingTexture(MG_Pipe::MGPipeHandle texture);
#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