[Fix] (Pipe, State): take the texture and framebuffer families through the contract own birth hooks, wire the texture subsystem bit, hand the applier the region tail it was promised and clear a level dirty flag only where the record was accepted

This commit is contained in:
2026-09-08 16:52:11 -04:00
committed by rereview
parent 2d2090cf1c
commit 771e8e06a1
6 changed files with 639 additions and 385 deletions
+198 -33
View File
@@ -64,22 +64,23 @@ namespace MobileGL::MG_Pipe {
// D-C1: the MGPSurface builder, one pure function, one statement per field
// ---------------------------------------------------------------------------------
// MGPSurface::Kind. MGPipeKind is REUSED rather than a second three-value enum minted
// beside it: it already spells Texture and Renderbuffer, its None is 0, and a
// zero-initialised MGPSurface therefore already IS the empty attachment point the contract
// describes ({Res = kMGPipeNullHandle, Kind = None} and every other field zero). If the
// contract package later wants a dedicated enumeration beside MGPSurface it is a rename,
// not a re-encoding.
inline constexpr Uint8 kMGPipeSurfaceKindNone = static_cast<Uint8>(MGPipeKind::None);
inline constexpr Uint8 kMGPipeSurfaceKindTexture = static_cast<Uint8>(MGPipeKind::Texture);
inline constexpr Uint8 kMGPipeSurfaceKindRenderbuffer = static_cast<Uint8>(MGPipeKind::Renderbuffer);
static_assert(kMGPipeSurfaceKindNone == 0,
"a zero-initialised MGPSurface must already be the empty attachment point");
// MGPSurface::Kind's three constants ARE THE CONTRACT'S (ID-12 DV-4, c0c):
// kMGPipeSurfaceKindNone / ...Texture / ...Renderbuffer live in MG_Pipe/MGPipeTypes.h under
// exactly these names with the same MGPipeKind derivation and the same static_assert. This
// package's copies were a redefinition in the same namespace and are deleted.
// The upload target an attachment names, RESOLVED: an attachment made through an entry
// point that carries no face token stores TextureUploadTarget::Unknown, and the record goes
// out fully resolved - nothing in it may require a lookup on the far side. This is the same
// fallback FramebufferAttachmentObject::GetSize already applies to answer its own question.
// out fully resolved - nothing in it may require a lookup on the far side.
//
// THE FALLBACK IS ONLY LEGAL FOR A SINGLE-TARGET TEXTURE (m1), and v1's was not. The
// precedent it copied - FramebufferAttachmentObject::GetSize - needs an EXTENT, which is
// identical across a cube map's six faces; face IDENTITY is not, so
// `glFramebufferTexture(GL_COLOR_ATTACHMENT0, cube, 0)` resolved to targets[0] and the
// record ASSERTED CubeMapPositiveX for a layered attachment that names all six. A texture
// with exactly one upload target has a [0] that IS the truth; anything else keeps Unknown,
// which is the value the field already carries for "this attachment names no single face"
// and which Layered = 1 tells the reader to ignore.
inline MobileGL::TextureUploadTarget MGPipeResolveAttachmentUploadTarget(
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
MobileGL::TextureUploadTarget resolved = attachment.GetTextureUploadTarget();
@@ -87,7 +88,7 @@ namespace MobileGL::MG_Pipe {
const auto& texture = attachment.GetTexture();
if (!texture) return MobileGL::TextureUploadTarget::Unknown;
const auto& targets = texture->GetUploadTargets();
return targets.empty() ? MobileGL::TextureUploadTarget::Unknown : targets[0];
return targets.size() == 1 ? targets[0] : MobileGL::TextureUploadTarget::Unknown;
}
// ONE PURE FUNCTION, ONE STATEMENT PER FIELD, and that shape is a gate requirement rather
@@ -98,9 +99,22 @@ namespace MobileGL::MG_Pipe {
// `res` is handed in because resolving it needs the slot allocator and this function stays
// pure; `internalFormat` is INLINE in the record on purpose, so the four cross-object masks
// fall out at push time with no lookup on the far side.
// THE EMPTY POINT IS THE ZERO-INITIALISED RECORD EXCEPT FOR ITS TWO TARGET FIELDS. Both
// are Uint16 enumerations whose zero is a REAL value - TextureTarget::Texture1D and
// TextureUploadTarget::Texture1D - so a reader that forgot to gate on Kind would read a
// plausible wrong answer rather than a nonsense one. Unknown (0xFFFF) is what the contract
// spells for TextureTarget (kMGPipeSurfaceNoTextureTarget) and m6 applies the same rule to
// UploadTarget, which shares the collision ID-12 DV-3 ruled on for MGPSubData::Target.
inline MGPSurface MGPipeEmptySurface() {
MGPSurface surface{};
surface.UploadTarget = static_cast<Uint16>(MobileGL::TextureUploadTarget::Unknown);
surface.TextureTarget = kMGPipeSurfaceNoTextureTarget;
return surface;
}
inline MGPSurface MGPipeBuildSurface(const MG_State::GLState::FramebufferAttachmentObject& attachment,
MGPipeHandle res) {
MGPSurface surface{};
MGPSurface surface = MGPipeEmptySurface();
if (attachment.IsEmpty()) return surface;
surface.Res = res;
if (attachment.IsTexture()) {
@@ -111,6 +125,13 @@ namespace MobileGL::MG_Pipe {
surface.Level = static_cast<Uint16>(std::max<Int>(attachment.GetTextureLevel(), 0));
surface.Layer = static_cast<Uint32>(std::max<Int>(attachment.GetTextureLayer(), 0));
surface.UploadTarget = static_cast<Uint16>(MGPipeResolveAttachmentUploadTarget(attachment));
// ID-12 DV-5: the field that WAS Pad0, and the size did not move. The four
// cross-object masks all reduce to (format, TEXTURE TARGET) -
// ShouldUseCaveatTextureFormat / BackendTextureFormatAddsAlpha - and no
// TextureUploadTarget -> TextureTarget inverse exists anywhere in the tree, so
// without this the inline InternalFormat cannot make them fall out at push time and
// the backend keeps reading the frontend attachment objects.
surface.TextureTarget = static_cast<Uint16>(texture->GetTarget());
return surface;
}
const auto& renderbuffer = attachment.GetRenderbuffer();
@@ -119,7 +140,6 @@ namespace MobileGL::MG_Pipe {
surface.Layered = 0;
surface.Level = 0;
surface.Layer = 0;
surface.UploadTarget = 0;
return surface;
}
@@ -142,6 +162,22 @@ namespace MobileGL::MG_Pipe {
return 0;
}
// m2: A DRAW-BUFFER TOKEN CAN NAME A COLOUR POINT THE RECORD CANNOT CARRY, and D-C3's
// refusal loop only ever scanned ATTACHMENTS. `glDrawBuffers(1, {GL_COLOR_ATTACHMENT10})`
// with nothing attached at 10 is legal state - draw-incomplete, but legal - and the index
// above would have written 10 into a record whose Color[] is 8 wide, so the server would
// index out of its own storage or invent a bound the record does not carry. Truncating
// silently is the bug class this phase is closing, so the record is refused exactly as an
// over-wide attachment is.
inline Bool MGPipeDrawBufferIsInsideTheWireWidth(MobileGL::FramebufferAttachmentType buffer) {
using MobileGL::FramebufferAttachmentType;
if (buffer < FramebufferAttachmentType::Color0 || buffer > FramebufferAttachmentType::ColorMax) {
return true; // None and the four default-framebuffer tokens; neither indexes Color[]
}
return static_cast<Int>(buffer) - static_cast<Int>(FramebufferAttachmentType::Color0) <
static_cast<Int>(kMGPipeMaxColorAttachments);
}
// ---------------------------------------------------------------------------------
// D-C4: ContentHash, and the one input it must not swallow
// ---------------------------------------------------------------------------------
@@ -169,6 +205,11 @@ namespace MobileGL::MG_Pipe {
dst.Level = src.Level;
dst.Layer = src.Layer;
dst.UploadTarget = src.UploadTarget;
// MANDATORY, not optional: TextureTarget is a PipeFields.def row now, so a
// field-wise copy that skipped it would suppress a record whose only moved field is
// the attachment's texture target - and that field decides three of the four
// cross-object masks.
dst.TextureTarget = src.TextureTarget;
}
inline Uint64 MGPipeFramebufferStateContentHash(const MGPFramebufferState& state) {
@@ -234,14 +275,13 @@ namespace MobileGL::MG_Pipe {
Bool drawOk = false;
Bool readOk = false;
if (shared) {
drawOk = BuildFramebufferState(*drawFbo, *drawFbo, MGPipeFramebufferTarget::Both, drawState);
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Both, drawState);
} else {
if (drawFbo) {
drawOk = BuildFramebufferState(*drawFbo, readFbo ? *readFbo : *drawFbo,
MGPipeFramebufferTarget::Draw, drawState);
drawOk = BuildFramebufferState(*drawFbo, MGPipeFramebufferTarget::Draw, drawState);
}
if (readFbo) {
readOk = BuildFramebufferState(*readFbo, *readFbo, MGPipeFramebufferTarget::Read, readState);
readOk = BuildFramebufferState(*readFbo, MGPipeFramebufferTarget::Read, readState);
}
}
if (!drawOk && !readOk) return 0;
@@ -279,10 +319,79 @@ namespace MobileGL::MG_Pipe {
return bytes;
}
// ID-19(c): EVERY DSA ENTRY POINT THAT HANDS A FRAMEBUFFER TO THE SERVER BY NAME IS
// PRECEDED BY A RECORD FOR IT, and that is the phase's main correction rather than a
// nicety. With only the two BOUND-target records, glClearNamedFramebufferfv(fbo) on an
// unbound fbo made the backend mint a fresh driver framebuffer with NO ATTACHMENTS,
// find no record for it, decline, and issue the clear against it anyway -
// GL_INVALID_FRAMEBUFFER_OPERATION and nothing cleared, where the legacy arm cleared
// correctly (esprytobj C-1).
//
// THE TARGET IS Named ONLY WHEN THE OBJECT IS BOUND TO NEITHER BINDING. A record always
// writes FramebufferRecords[Fbo.Slot]; Draw/Read/Both ADDITIONALLY set the bound
// handle(s). So handing a currently-bound framebuffer a Named record would overwrite
// the bound record's Target with one that says "no binding" while BoundFramebuffer
// still names it, and the server would read a record whose Target contradicts the
// binding it is resolved through. Re-asserting the binding the object already has is
// free (the content hash suppresses it) and keeps the two consistent.
//
// Returns the bytes that went on the wire.
Uint64 EmitFramebufferByName(const FramebufferObject& fbo) {
if (!MGPipeFramebufferSubsystemEnabled()) return 0;
MGPipeFramebufferTarget target = MGPipeFramebufferTarget::Named;
const Bool boundToDraw = IsBoundTo(fbo, MobileGL::FramebufferTarget::Draw);
const Bool boundToRead = IsBoundTo(fbo, MobileGL::FramebufferTarget::Read);
if (boundToDraw && boundToRead) {
target = MGPipeFramebufferTarget::Both;
} else if (boundToDraw) {
target = MGPipeFramebufferTarget::Draw;
} else if (boundToRead) {
target = MGPipeFramebufferTarget::Read;
}
MGPFramebufferState state{};
if (!BuildFramebufferState(fbo, target, state)) return 0;
// THE SUPPRESSOR IS KEYED BY THE FRAMEBUFFER THE RECORD NAMES, never by one global
// slot (MGPipeTypes.h states the rule): two different objects' Named records in a
// row must both go out, and a Named record must never be suppressed against the
// same object's bound record or the reverse. Target is a ContentHash input, so the
// second half holds by construction; the per-object table is what buys the first.
// The two BOUND latches stay what they are - "does the server's draw/read binding
// already hold this record" - and a bound-target emission from here consults them,
// because a rebind of an unchanged object must still move the binding.
if (target == MGPipeFramebufferTarget::Named) {
NamedEntry& entry = NamedEntryFor(state.Fbo);
if (entry.Has && entry.Gen == state.Fbo.Gen && entry.LastHash == state.ContentHash) {
return 0;
}
const Uint64 bytes = Emit(state);
entry.Has = true;
entry.Gen = state.Fbo.Gen;
entry.LastHash = state.ContentHash;
return bytes;
}
if (target == MGPipeFramebufferTarget::Both) {
if (state.ContentHash == m_lastEmitted[kDraw] && state.ContentHash == m_lastEmitted[kRead]) {
return 0;
}
const Uint64 bytes = Emit(state);
m_lastEmitted[kDraw] = state.ContentHash;
m_lastEmitted[kRead] = state.ContentHash;
return bytes;
}
const SizeT slot = target == MGPipeFramebufferTarget::Read ? kRead : kDraw;
if (state.ContentHash == m_lastEmitted[slot]) return 0;
const Uint64 bytes = Emit(state);
m_lastEmitted[slot] = state.ContentHash;
return bytes;
}
// ---- what a unit case reads. The emitter builds INTO these and hands the applier the
// same objects, so "what was emitted" costs no copy. ----
const MGPFramebufferState& LastDraw() const { return m_lastDraw; }
const MGPFramebufferState& LastRead() const { return m_lastRead; }
const MGPFramebufferState& LastNamed() const { return m_lastNamed; }
Uint64 EmissionCount() const { return m_emissions; }
Uint64 RefusedCount() const { return m_refusals; }
@@ -295,6 +404,13 @@ namespace MobileGL::MG_Pipe {
void Reset() {
m_lastEmitted[kDraw] = 0;
m_lastEmitted[kRead] = 0;
// The per-object latch goes too, and the safe direction is why: MGPipeApplierReset
// keeps FramebufferRecords standing (they are object state, ID-19(b)) but
// ReleaseObjectRecords clears the whole table, and this emitter cannot tell the two
// scopes apart from here. Keeping a latch across a table that may have been dropped
// would suppress the one record that had to go out; dropping it costs one extra
// 304-byte record per named framebuffer after a context switch.
m_named.clear();
}
void ResetCounters() { m_emissions = m_refusals = 0; }
@@ -304,6 +420,7 @@ namespace MobileGL::MG_Pipe {
ResetCounters();
m_lastDraw = MGPFramebufferState{};
m_lastRead = MGPFramebufferState{};
m_lastNamed = MGPFramebufferState{};
}
private:
@@ -311,7 +428,9 @@ namespace MobileGL::MG_Pipe {
static constexpr SizeT kRead = 1;
Uint64 Emit(const MGPFramebufferState& state) {
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read)) {
if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Named)) {
m_lastNamed = state;
} else if (state.Target == static_cast<Uint8>(MGPipeFramebufferTarget::Read)) {
m_lastRead = state;
} else {
m_lastDraw = state;
@@ -325,16 +444,20 @@ namespace MobileGL::MG_Pipe {
return sizeof(MGPFramebufferState);
}
// `fbo` is the object this record describes; `readFbo` is the object whose OWN read
// buffer resolves ReadSurface, and for a Draw record that is the read-bound object
// rather than this one.
// ONE RECORD DESCRIBES ONE FRAMEBUFFER OBJECT - the one named by `fbo` - and every
// field in it is a property of THAT object. Target is the only binding-specific one.
//
// THE RESOLVED READ SURFACE IS WHAT STRUCTURALLY CLOSES THE read-buffer-shared-FBO
// DEFECT CLASS: the record carries the surface, not an index, and it is resolved from
// the READ framebuffer's own read buffer, so the "same FBO as draw" skip that used to
// lose it cannot be expressed.
Bool BuildFramebufferState(const FramebufferObject& fbo, const FramebufferObject& readFbo,
MGPipeFramebufferTarget target, MGPFramebufferState& out) {
// ReadSurface IS RESOLVED FROM THIS FRAMEBUFFER'S OWN READ BUFFER UNDER EVERY TARGET,
// Named included (c0e / MGPipeTypes.h). v1 resolved a Draw record's ReadSurface from
// the READ-bound object, which was D-C2's letter and muddled in substance: the record
// then described a surface that is not part of the framebuffer its own Fbo names, and a
// glReadBuffer on the read FBO moved the DRAW record's ContentHash and forced a
// redundant draw emission. Resolving it per object is what makes the
// read-buffer-shared-FBO defect class unrepresentable rather than merely fixed - the
// record carries a surface, not an index, and no field of it refers to "whatever is
// bound".
Bool BuildFramebufferState(const FramebufferObject& fbo, MGPipeFramebufferTarget target,
MGPFramebufferState& out) {
// D-C3, THE CLIENT HALF OF THE BRING-UP REFUSAL. The wire array is 8 wide and
// GetDynamicParameters().MaxColorAttachments is the driver's raw ES cap, not
// clamped to 8 on the GLES path. An attachment point at or above the wire width
@@ -356,6 +479,25 @@ namespace MobileGL::MG_Pipe {
return false;
}
// m2, THE SAME REFUSAL ONE FIELD OVER. A draw-buffer token may name a colour point
// at or above the wire width with nothing attached there, which the loop above
// cannot see; MGPipeDrawBufferIndex would then write 8..31 into an 8-wide array.
{
const auto& tokens = fbo.GetDrawBuffers();
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
if (MGPipeDrawBufferIsInsideTheWireWidth(tokens[i])) continue;
MGLOG_E_ONCE("MGPipe: framebuffer %u names colour point %d in draw buffer %u, which is "
"at or above the wire width of %u - set_framebuffer_state is refused "
"rather than truncated and the legacy arm runs",
fbo.GetExternalIndex(),
static_cast<Int>(tokens[i]) -
static_cast<Int>(FramebufferAttachmentType::Color0),
static_cast<Uint>(i), static_cast<Uint>(kMGPipeMaxColorAttachments));
++m_refusals;
return false;
}
}
out = MGPFramebufferState{};
out.Fbo = HandleFor(fbo);
out.Target = static_cast<Uint8>(target);
@@ -376,7 +518,7 @@ namespace MobileGL::MG_Pipe {
}
out.Depth = SurfaceOf(fbo, FramebufferAttachmentType::Depth);
out.Stencil = SurfaceOf(fbo, FramebufferAttachmentType::Stencil);
out.ReadSurface = SurfaceOf(readFbo, readFbo.GetReadBuffer());
out.ReadSurface = SurfaceOf(fbo, fbo.GetReadBuffer());
const auto& drawBuffers = fbo.GetDrawBuffers();
for (SizeT i = 0; i < kMGPipeMaxColorAttachments; ++i) {
@@ -395,12 +537,30 @@ namespace MobileGL::MG_Pipe {
return true;
}
static Bool IsBoundTo(const FramebufferObject& fbo, MobileGL::FramebufferTarget target) {
if (MG_State::pGLContext == nullptr) return false;
const auto& bound = MG_State::pGLContext->GetFramebufferBindingSlot(target).GetBoundObject();
return bound && bound.get() == &fbo;
}
struct NamedEntry {
Uint32 Gen = 0;
Uint64 LastHash = 0;
Bool Has = false;
};
NamedEntry& NamedEntryFor(MGPipeHandle fbo) {
const SizeT slot = fbo.Slot;
if (slot >= m_named.size()) m_named.resize(slot + 1);
return m_named[slot];
}
MGPSurface SurfaceOf(const FramebufferObject& fbo, FramebufferAttachmentType type) {
if (type == FramebufferAttachmentType::None || type == FramebufferAttachmentType::Unknown) {
return MGPSurface{};
return MGPipeEmptySurface();
}
const auto& attachment = fbo.GetAttachment(type);
if (attachment.IsEmpty()) return MGPSurface{};
if (attachment.IsEmpty()) return MGPipeEmptySurface();
MGPipeTextureEmitter& textures = MGPipeTextureEmitterInstance();
// D-A4's two producers: an attachment point is what sets RENDER_TARGET and
// DEPTH_STENCIL, the two sticky bind bits nothing set before P4a. Sticky and ORed,
@@ -457,8 +617,13 @@ namespace MobileGL::MG_Pipe {
}
Array<Uint64, 2> m_lastEmitted{};
// The per-FRAMEBUFFER suppressor for Named records, slot-indexed with the generation
// checked, exactly as the applier's own table is. A framebuffer has no wire lifetime
// (D-I2), so a successor simply overwrites its predecessor's entry.
Vector<NamedEntry> m_named;
MGPFramebufferState m_lastDraw{};
MGPFramebufferState m_lastRead{};
MGPFramebufferState m_lastNamed{};
Uint64 m_emissions = 0;
Uint64 m_refusals = 0;
};
+373 -299
View File
@@ -27,28 +27,33 @@
// HEADER-ONLY, for the ownership reason Tracker.h and ResourceTracker.h both state.
//
// ---------------------------------------------------------------------------------------
// HOW MG_State REACHES THIS FILE, and it is a DEVIATION worth reading before the code.
// HOW MG_State REACHES THIS FILE: IT DOES NOT, AND THAT IS THE POINT (c0b, ID-13).
//
// P3a's buffer family declares its emission points in MG_Pipe/PipeMutation.h and defines them
// in MG_Impl/Pipe/PipeFill.cpp, so BufferObject.cpp sees a declaration and never the client's
// tracker. P4a cannot copy that shape: BOTH of those files belong to the contract package for
// the whole phase (no file is touched twice by two packages), and they carry no texture
// emission declaration. The next-best arrangement, and the one used here, keeps the SAME
// property one level in:
// v1 of this package shipped a deviation - six free functions here, called from
// TextureObject.cpp and RenderbufferObject.cpp - because at the contract TAG
// MG_Pipe/PipeMutation.h declared only the six DEATH helpers and this package may not edit
// A's files. c0b landed the BIRTH half, so the deviation is retired rather than carried:
// MG_State now calls MGPipeMintTextureHandle / MGPipeEmitTextureResourceCreate /
// ...ResourceRespecify / MGPipeEmitTextureParams / MGPipeNoteTextureLevelDirty and the two
// renderbuffer twins, all DECLARED in MG_Pipe/PipeMutation.h and DEFINED in
// MG_Impl/Pipe/PipeFill.cpp, which forwards to the entry points below through
// ForwardWhenWired<kMGPipeWiredTextureSubsystem>. No MG_State translation unit includes this
// header any more, which is the property check_include_closure.py's mutation-header probe
// exists to keep.
//
// * every texture emission point is a protected member of TextureObjectBase
// (TextureState/TextureObject.h, guarded by MOBILEGL_PIPE_PUSH, non-virtual, so the pull
// build's object layout and vtable are untouched) DECLARED there and DEFINED in
// TextureState/TextureObject.cpp - the ONE MG_State translation unit that includes this
// header. Every other texture .cpp - the cube's, the view's, the buffer texture's - calls
// the inherited helper and still sees only a declaration.
// * the renderbuffer half has no base class to hang helpers on and exactly one .cpp, so
// RenderbufferState/RenderbufferObject.cpp is the second and last such translation unit.
// WHAT THIS FILE OWES THAT SEAM, and a mismatch is a compile error in this package's own
// commit rather than a surprise at the merge (that is what the wired constant buys):
//
// So the client's tracker reaches exactly two MG_State translation units instead of zero. The
// integrator can fold these six free functions into PipeMutation.h and PipeFill.cpp in one
// mechanical commit once the phase's file ownership relaxes; nothing else has to move.
// EmitResourceCreate(ITextureObject&) EmitResourceRespecify(ITextureObject&)
// EmitTextureParams(ITextureObject&) NoteLevelDirty(ITextureObject&, Uint32, Uint32)
// EmitRenderbufferCreate(RenderbufferObject&)
// EmitRenderbufferRespecify(RenderbufferObject&)
//
// and the PUBLICATION LATCH is A's too: MGPipeNoteHandlePublished is called where a create
// actually goes out and MGPipeHandleIsPublished is what the death helpers read, so this file
// keeps no Published flag of its own.
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SamplerEmit.h>
#include <MG_Impl/Pipe/SlotAllocator.h>
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h>
@@ -72,38 +77,37 @@ namespace MobileGL::MG_Pipe {
// emitters their bodies, with no file touched twice - and a Coverage.def row can never
// silently drop a field on the floor before the call that carries it exists.
//
// IT IS STILL 0, AND THAT IS A BLOCKED FLIP RATHER THAN AN UNFINISHED ONE. Unlike the other
// three P4a families, this one does not get four fresh apply entry points: the catalogue is
// closed and a texture rides P3a's OWN resource_create / resource_respecify /
// resource_subdata / resource_destroy rows. On a base without the wire package's `w1` those
// four have P3a's BUFFER bodies, and two of their properties make a texture record actively
// harmful rather than merely ignored:
// IT WAS 0 IN v1, AND THE THING THAT BLOCKED IT IS NOW IN THE TREE. Unlike the other three
// P4a families this one gets no fresh apply entry points - the catalogue is closed and a
// texture rides P3a's OWN resource_create / resource_respecify / resource_subdata /
// resource_destroy rows - so on a base without the wire package's per-kind record vectors
// and its texture branch, two properties made a texture record actively harmful rather than
// merely ignored: MGPipeApplierState::Resources was ONE slot-indexed vector, so a texture
// create at slot 12 overwrote the BUFFER record at slot 12; and SubDataBoxFault validated
// every record as the buffer half of MGPSubData, so a texture sub-data record was
// Fatal{ProtocolCorruption} on `record.Level != 0` alone. Both are closed on this base
// (three per-kind vectors, ApplyTextureUpload, SubDataTextureFault, and C1's level-scoped
// PendingUploads clear), so the constant is its own bit and the family is live.
//
// * MGPipeApplierState::Resources is ONE vector indexed by SLOT (D-B2 makes it three, one
// per resource kind). Buffer, Texture and Renderbuffer slot spaces are independent, so
// a texture create at slot 12 OVERWRITES the buffer record at slot 12, and the next
// write to that buffer is refused against the texture's extent - a dropped content
// write with no diagnostic beyond the refusal counter;
// * SubDataBoxFault validates every record as the buffer half of MGPSubData, so a texture
// sub-data record is Fatal{ProtocolCorruption} on `record.Level != 0` alone (D-D4's
// drain cases and TextureTest.GetTexImageReadsALevelWhoseLowerLevelsWereNeverDefined
// abort in a verify build, which is how this was found rather than argued).
//
// So the flip is `w1`'s to unblock and the integrator's to make, in the rebase of this
// branch onto the wire branch: change the 0 below to kMGPipeSubsystemTextureResources, and
// nothing else. The whole conversion is already gated by TextureEmit's cases, which arm the
// emitter directly (ArmForTest) and assert on the EMITTED records rather than on applier
// state - so the flip cannot land untested, and until it lands nothing this file builds
// reaches an applier that cannot hold it.
inline constexpr Uint64 kMGPipeWiredTextureSubsystem = 0;
// THE FLIP IS THE WHOLE SWITCH AND NOTHING ELSE MOVES: PipeFill.cpp ORs this constant into
// kMGPipeWiredSubsystems, static_asserts it is 0-or-its-own-bit, gates every birth hook on
// FamilyIsLive(kMGPipeSubsystemTextureResources, this), and gates DrainTextureSubData on
// the same OR. Bit 9 (framebuffer) REQUIRES bit 10 (D-K2), because every MGPSurface::Res
// names a texture or renderbuffer handle the applier must hold a record for - so this
// package may never be integrated with only one of the two constants set.
inline constexpr Uint64 kMGPipeWiredTextureSubsystem = kMGPipeSubsystemTextureResources;
static_assert(kMGPipeWiredTextureSubsystem == 0 ||
kMGPipeWiredTextureSubsystem == kMGPipeSubsystemTextureResources,
"a family's wired constant is 0 or its own bit and nothing else");
// BOTH HALVES MATTER, exactly as MGPipeResourceSubsystemEnabled()'s two do. The bit is the
// operator's per-subsystem A/B; the emitter's arm is "has this build's texture family been
// switched on at all", and it is initialised from the constant above. There is no third
// half - no MGPipeResourceOps member and no backend op table (D-B1: every P4a call is an
// object record or working state the applier stores, and none of them dispatches to a
// backend function pointer) - which is what makes the A/B a pure configuration question
// rather than a bring-up-order one.
// BOTH HALVES MATTER, exactly as MGPipeResourceSubsystemEnabled()'s two do, and they are
// the SAME PAIR PipeFill.cpp's FamilyIsLive applies - the operator's per-subsystem A/B bit
// in MOBILEGL_PIPE_PUSH, and this build having wired the family at all. There is no third
// half: v1 carried a runtime `m_armed` latch so a unit case could drive the conversion on a
// base whose applier could not hold the record, and with the constant flipped that latch
// would only be able to LIE (PipeFill.cpp's gate does not consult it, so an unarmed emitter
// would still be driven by every frontend mutation). It is deleted; a case that wants the
// family off clears MG_Config::Features.PipePush, which is the switch the shipped build has.
inline Bool MGPipeTextureSubsystemEnabled();
// DO THE RECORDS THIS EMITTER BUILDS REACH THE APPLIER ON THIS BASE? It is the wired
@@ -144,43 +148,17 @@ namespace MobileGL::MG_Pipe {
: MobileGL::TextureStorageType::Mipmap);
}
// MGPSubData::Target, for a TEXTURE, and the packing is stated here because the payload has
// exactly one Uint16 for two facts the server needs: WHICH KIND of storage the destination
// is (the applier branches on it - a buffer target dispatches into MGPipeResourceOps, every
// other target stores and returns) and WHICH CUBE FACE / upload target the level belongs
// to, which is not derivable from the resource target at all.
// MGPSubData::Target's PACKING AND THE TWO DEPTH-STENCIL NUMBERS ARE THE CONTRACT'S NOW
// (ID-12 DV-2/DV-3, c0c): MGPipePackSubDataTarget / MGPipeSubDataResourceTargetOf /
// MGPipeSubDataUploadTargetOf and kMGPipeDepthStencilModeDepth/Stencil live in
// MG_Pipe/MGPipeTypes.h under exactly these names, with Uint32 arguments so the header
// stays backend-neutral. This package's copies were the same spelling in the same
// namespace - a redefinition - and are deleted; the packing argument they carried is
// stated where the definitions now are.
//
// low byte = MGPipeResourceTarget (0 == Buffer, and P3a's buffer records are
// unchanged: their upload byte is 0 too, so a
// buffer record still compares == 0 whole)
// high byte = MobileGL::TextureUploadTarget (26 enumerators; a byte is ample)
//
// The buffer half of the encoding is what keeps this backward-compatible with
// MGPipeBuildSubDataRecord's `out.Target = kMGPipeResourceTargetBuffer`, so the applier's
// buffer branch can keep testing the whole field or only the low byte and be right either
// way.
inline constexpr Uint16 MGPipePackSubDataTarget(Uint32 resourceTarget,
MobileGL::TextureUploadTarget uploadTarget) {
return static_cast<Uint16>((resourceTarget & 0xFFu) |
((static_cast<Uint32>(uploadTarget) & 0xFFu) << 8));
}
inline constexpr Uint8 MGPipeSubDataResourceTargetOf(Uint16 packed) {
return static_cast<Uint8>(packed & 0xFFu);
}
inline constexpr Uint8 MGPipeSubDataUploadTargetOf(Uint16 packed) {
return static_cast<Uint8>((packed >> 8) & 0xFFu);
}
static_assert(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer,
static_cast<MobileGL::TextureUploadTarget>(0)) ==
kMGPipeResourceTargetBuffer,
"a buffer sub-data record's Target must stay exactly kMGPipeResourceTargetBuffer");
// MGPTextureParams::DepthStencilMode. The frontend keeps a GLenum (GL_DEPTH_COMPONENT /
// GL_STENCIL_INDEX, 0x1902 / 0x1901) and the payload byte cannot hold one, so the two
// legal values are numbered - 0 is DEPTH_COMPONENT, which is also the GL initial value and
// therefore what a zero-initialised record already says.
inline constexpr Uint8 kMGPipeDepthStencilModeDepth = 0;
inline constexpr Uint8 kMGPipeDepthStencilModeStencil = 1;
// WHAT STAYS HERE is the GLenum -> byte translation, which is frontend knowledge: the
// frontend keeps GL_DEPTH_COMPONENT / GL_STENCIL_INDEX (0x1902 / 0x1901) and the payload
// byte cannot hold one.
inline Uint8 MGPipeDepthStencilModeByte(GLenum mode) {
return mode == GL_STENCIL_INDEX ? kMGPipeDepthStencilModeStencil : kMGPipeDepthStencilModeDepth;
}
@@ -442,6 +420,7 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle AcquireTexture(Uint64 lifetimeId, ITextureObject* object) {
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Texture, lifetimeId);
Entry& entry = EntryFor(m_textures, handle);
RetireIfRecycled(entry, handle);
entry.Texture = object;
entry.Gen = handle.Gen;
return handle;
@@ -452,6 +431,7 @@ namespace MobileGL::MG_Pipe {
MGPipeHandle AcquireRenderbuffer(Uint64 lifetimeId) {
const MGPipeHandle handle = MGPipeSlots().Acquire(MGPipeKind::Renderbuffer, lifetimeId);
Entry& entry = EntryFor(m_renderbuffers, handle);
RetireIfRecycled(entry, handle);
entry.Gen = handle.Gen;
return handle;
}
@@ -479,77 +459,68 @@ namespace MobileGL::MG_Pipe {
// P4a get their producers here and in the framebuffer emitter: RENDER_TARGET and
// DEPTH_STENCIL from an attachment point, SAMPLER from a resolved sampler view and
// SHADER_IMAGE from a resolved image unit (the sampler package's two).
// A MASK CHANGE AFTER THE ALLOCATION IS A METADATA RESPECIFY (ID-18 M4), and without it
// the sticky half of D-A4 is a no-op for exactly the textures it was written for. The
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
// no further respecify, that being what immutable means - so for the canonical order
// `glTexStorage2D(...); glBindImageTexture(...)` the applier's record kept
// ImageBindableHint = 0 for ever and the PREVENTION half of the texture-remint stall
// class never fired. So a mask that actually MOVES re-emits the stored descriptor with
// the new mask: every storage-defining field is byte-identical to what the applier
// holds, which is exactly the shape wire applies as a METADATA UPDATE - the descriptor
// is replaced, no reallocation is acked, and NO pending upload is dropped, so a mask
// change arriving between a glTexSubImage2D and the sync that consumes it cannot eat
// the texels.
void NoteTextureBoundAs(MGPipeHandle handle, Uint16 bit) {
if (MGPipeHandleIsNull(handle)) return;
Entry& entry = EntryFor(m_textures, handle);
const Uint16 before = entry.BindMask;
entry.BindMask = static_cast<Uint16>(before | bit);
const Uint16 now = static_cast<Uint16>(before | bit);
if (now == before) return;
entry.BindMask = now;
// AN ImageBindableHint TRANSITION IS THE ONE THING THE CLIENT ASKS A RESYNC FOR
// (D-E2): the widened-channel carrier needs a swizzle override that the frontend's
// own params version does not move for, so the transition arms ForceResync on the
// next set_texture_params rather than being silently folded into the descriptor.
// SamplerResync stays the SERVER's byte and is never set from here.
if ((before & kMGPipeBindShaderImage) == 0 && (bit & kMGPipeBindShaderImage) != 0) {
entry.ForceParamsResync = true;
}
RepublishMask(MGPipeKind::Texture, handle, entry);
}
void NoteRenderbufferBoundAs(MGPipeHandle handle, Uint16 bit) {
if (MGPipeHandleIsNull(handle)) return;
Entry& entry = EntryFor(m_renderbuffers, handle);
entry.BindMask = static_cast<Uint16>(entry.BindMask | bit);
const Uint16 before = entry.BindMask;
const Uint16 now = static_cast<Uint16>(before | bit);
if (now == before) return;
entry.BindMask = now;
RepublishMask(MGPipeKind::Renderbuffer, handle, entry);
}
Uint16 TextureBindMask(MGPipeHandle handle) const { return MaskOf(m_textures, handle); }
Uint16 RenderbufferBindMask(MGPipeHandle handle) const { return MaskOf(m_renderbuffers, handle); }
// ---- the publication latch (D-I1) ----
//
// The create is gated at its call site and the destroy inside the death helper, so the
// two ask the SAME question at two different moments. An object born while the
// subsystem bit was clear and destroyed after it was set would otherwise free its slot
// with the applier's record still Live, on a slot about to be handed out again. So the
// answer is LATCHED at the create and the destroy uses the latched one.
Bool TextureRecordIsPublished(MGPipeHandle handle) const {
return PublishedIn(m_textures, handle);
}
Bool RenderbufferRecordIsPublished(MGPipeHandle handle) const {
return PublishedIn(m_renderbuffers, handle);
}
void NoteTextureRecordDestroyed(MGPipeHandle handle) { Retire(m_textures, handle); }
void NoteRenderbufferRecordDestroyed(MGPipeHandle handle) { Retire(m_renderbuffers, handle); }
// ---- the three object calls (the entry points PipeFill.cpp forwards to) ----
// ---- the three object calls ----
void EmitTextureCreate(ITextureObject& texture) {
// resource_create, from TextureObjectBase's CONSTRUCTOR - so the DERIVED object does
// not exist yet and only members TextureObjectBase itself implements may be read.
// GetTarget(), GetExternalIndex() and GetLifetimeId() are all overridden ON THE BASE,
// so they dispatch to the base's own bodies here and read members the mem-init list has
// already written; GetStorageType() and GetUploadTargets() are NOT, so calling either
// would be undefined behaviour and the storage kind is derived from the target instead
// (exact, and re-checked at the first respecify where the object IS complete).
void EmitResourceCreate(ITextureObject& texture) {
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
Entry& entry = EntryFor(m_textures, handle);
const MGPResourceDesc desc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/false, kMGPipeNullHandle,
kMGPipeNullHandle, 0, 0);
entry.Published = true;
entry.LastDesc = desc;
entry.HasLastDesc = true;
NoteDesc(desc, /*isCreate=*/true);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(desc);
}
// resource_create for a texture whose DERIVED object does not exist yet - the base
// constructor. Nothing virtual is touched: the target and the GL name are the two
// facts a create carries and both are plain members by then. See
// MGPipeTextureStorageKindForTarget for why the storage kind may not be asked for here.
void EmitTextureCreateFromBase(Uint64 lifetimeId, ITextureObject* object,
MobileGL::TextureTarget target, Uint externalIndex) {
const MGPipeHandle handle = AcquireTexture(lifetimeId, object);
Entry& entry = EntryFor(m_textures, handle);
MGPResourceDesc desc{};
desc.Resource = handle;
desc.Target = static_cast<Uint8>(MGPipeResourceTargetForTextureTarget(target));
desc.StorageKind = MGPipeTextureStorageKindForTarget(target);
desc.Target = static_cast<Uint8>(MGPipeResourceTargetForTextureTarget(texture.GetTarget()));
desc.StorageKind = MGPipeTextureStorageKindForTarget(texture.GetTarget());
desc.BindMask = entry.BindMask;
desc.GlNameForDiag = static_cast<Uint32>(externalIndex);
entry.Published = true;
entry.LastDesc = desc;
entry.HasLastDesc = true;
desc.ImageBindableHint = (entry.BindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0;
desc.GlNameForDiag = static_cast<Uint32>(texture.GetExternalIndex());
NoteDesc(desc, /*isCreate=*/true);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(desc);
PublishCreate(MGPipeKind::Texture, handle, entry, desc);
}
// resource_respecify, from every storage-defining entry point. DEDUPED ON THE
@@ -559,9 +530,12 @@ namespace MobileGL::MG_Pipe {
// this record carries). A byte compare of an 88-byte POD is cheaper than the emission
// it avoids, and it is the same "version-first skip before anything expensive" shape
// every other P4a emission takes.
void EmitTextureRespecify(ITextureObject& texture) {
void EmitResourceRespecify(ITextureObject& texture) {
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
Entry& entry = EntryFor(m_textures, handle);
// THE VIEW'S OWNER IS ACQUIRED FIRST, and no Entry& is held across it (m3): the
// owner's slot can be higher than this table's size, so AcquireTexture would
// resize() the vector out from under a reference taken before it. Every Entry&
// below is taken after the last call that can grow the table.
MGPipeHandle viewOf = kMGPipeNullHandle;
if (const auto& owner = texture.GetViewStorageOwner()) {
// ONE HOP ALWAYS REACHES STORAGE: glTextureView composes a view-of-a-view onto
@@ -590,6 +564,7 @@ namespace MobileGL::MG_Pipe {
: static_cast<Uint64>(bufferTexture.GetBufferRangeSizeInBytes());
}
}
Entry& entry = EntryFor(m_textures, handle);
const MGPResourceDesc desc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/true, viewOf, bufferHandle, bufOffset,
bufSize);
@@ -598,21 +573,43 @@ namespace MobileGL::MG_Pipe {
// subsystem bit was clear has no applier record, and every later respecify would be
// REFUSED. A create rather than a respecify, because that is what the record's
// absence means and because the applier starts a record over on a create.
if (!entry.Published) {
if (!MGPipeHandleIsPublished(MGPipeKind::Texture, handle)) {
const MGPResourceDesc createDesc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/false, viewOf, bufferHandle,
bufOffset, bufSize);
entry.Published = true;
NoteDesc(createDesc, /*isCreate=*/true);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(createDesc);
PublishCreate(MGPipeKind::Texture, handle, entry, createDesc);
}
entry.LastDesc = desc;
entry.HasLastDesc = true;
NoteDesc(desc, /*isCreate=*/false);
// NO initial bytes: a texture's texels travel as resource_subdata out of the drain
// list, never inside its storage definition. This is what keeps glTexImage2D's
// "define the level and upload it" one allocation and one upload rather than two.
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceRespecify(desc, nullptr);
//
// AND THE MIRROR ONLY ADVANCES ON ACCEPTANCE (ID-18 M3): the dedupe above is a claim
// about what the APPLIER holds, so a refused respecify must leave LastDesc naming
// the descriptor that actually landed, or the next identical call is suppressed
// against a record that was never stored.
Bool accepted = ApplyRespecify(desc);
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
if (!accepted) {
// THE SECOND HALF OF THE SELF-HEAL, and the publication latch cannot give
// it: the latch answers "did a create for this handle GO OUT", which stays
// true after MGPipeApplierReleaseObjectRecords has dropped every object
// record - the scope a served context's teardown takes while the frontend
// objects live on in the share group. The applier's REFUSAL is the only
// signal that says "I hold nothing for this handle", and the acceptance
// return is what makes it visible from here at all. One retry, never a
// loop: a descriptor the applier refuses on its own merits (a target that
// names no resource kind) is refused again and the flags stay set.
const MGPResourceDesc healDesc = MGPipeBuildTextureResourceDesc(
texture, handle, entry.BindMask, /*storageDefined=*/false, viewOf,
bufferHandle, bufOffset, bufSize);
NoteDesc(healDesc, /*isCreate=*/true);
PublishCreate(MGPipeKind::Texture, handle, entry, healDesc);
accepted = ApplyRespecify(desc);
}
}
NoteRespecified(entry, desc, accepted);
}
void EmitTextureParams(ITextureObject& texture) {
@@ -628,15 +625,54 @@ namespace MobileGL::MG_Pipe {
texture.GetExternalIndex());
return;
}
// THE BUILT-IN SAMPLER'S CSO SLOT IS KEYED ON THE SamplerObject's OWN LIFETIME ID,
// which is the same key ~SamplerObject's death helper resolves through
// (MGPipeEmitSamplerCsoDestroyAndFree). The CALL that fills the record -
// create_sampler_state - is the sampler package's; minting the handle is client
// state and is this record's to name.
// THE VERSION-FIRST SKIP, AND IT READS BOTH COUNTERS (clientsp-v2 rule 4, and it is
// the M2 defect stated as a rule): glTexParameter* moves GetTextureParamsVersion()
// AND lands on the built-in SamplerObject, but the three fields this record takes
// off that object - MinLod, MaxLod, LodBias - are ALSO reachable through paths that
// move only SamplerObject::GetVersion(). Latching on the texture's counter alone is
// what let glTexParameterf(GL_TEXTURE_MIN_LOD) go stale. ForceParamsResync is the
// third input because an ImageBindableHint transition moves neither counter.
const Uint16 paramsVersion = texture.GetTextureParamsVersion();
const Uint16 samplerVersion = sampler->GetVersion();
if (entry.HasParamsLatch && entry.ParamsVersion == paramsVersion &&
entry.SamplerVersion == samplerVersion && !entry.ForceParamsResync) {
return;
}
entry.HasParamsLatch = true;
entry.ParamsVersion = paramsVersion;
entry.SamplerVersion = samplerVersion;
// ID-14 / ID-17: THE BUILT-IN SAMPLER COMES FROM C's CONTENT-ADDRESSED CACHE and is
// never minted here. v1 took MGPipeSlots().Acquire(SamplerCso, the SamplerObject's
// lifetime id), which is a slot no create_sampler_state ever names - so on the
// integrated tree every texture's params record would have carried a handle the
// applier holds nothing for. The cache mints and emits create_sampler_state on a
// miss, so the texture's built-in sampler and a glBindSampler'd object with the
// same value share ONE CSO and one server-side twin.
//
// EVERY Acquire TAKES A REFERENCE AND THIS ENTRY OWES EXACTLY ONE. The reference is
// what stops the LRU pulling a handle out from under a standing MGPTextureParams
// record: the applier deliberately does not resolve BuiltinSampler, and an eviction
// is not a parameter change, so nothing would refuse and nothing would re-emit. The
// previous handle is released when the content moves it, and the last one when the
// slot is recycled (RetireIfRecycled) - which is the only moment this package can
// see a texture die, the death helper being A's.
MGPipeSamplerCsoCache& cache = MGPipeSamplerCsoCacheInstance();
Uint64 samplerBytes = 0;
const MGPipeHandle builtinSampler =
MGPipeSlots().Acquire(MGPipeKind::SamplerCso, sampler->GetLifetimeId());
cache.Acquire(sampler->GetAllSamplerParameters(), samplerBytes);
m_samplerCsoPayloadBytes += samplerBytes;
if (entry.BuiltinSampler == builtinSampler) {
// The value did not move, so the cache handed back the handle this entry
// already pins AND a second reference for it. Give that one straight back.
cache.Release(builtinSampler);
} else {
cache.Release(entry.BuiltinSampler); // a no-op for the null handle
entry.BuiltinSampler = builtinSampler;
}
const MGPTextureParams params =
MGPipeBuildTextureParams(texture, handle, builtinSampler, entry.ForceParamsResync);
MGPipeBuildTextureParams(texture, handle, entry.BuiltinSampler, entry.ForceParamsResync);
entry.ForceParamsResync = false;
m_lastParams = params;
++m_paramSets;
@@ -649,11 +685,8 @@ namespace MobileGL::MG_Pipe {
const MGPResourceDesc desc = MGPipeBuildRenderbufferResourceDesc(renderbuffer, handle,
entry.BindMask,
/*storageDefined=*/false);
entry.Published = true;
entry.LastDesc = desc;
entry.HasLastDesc = true;
NoteDesc(desc, /*isCreate=*/true);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(desc);
PublishCreate(MGPipeKind::Renderbuffer, handle, entry, desc);
}
// D-D2: THE RENDERBUFFER PUBLICATION HOLE, CLOSED BY EMISSION.
@@ -673,17 +706,26 @@ namespace MobileGL::MG_Pipe {
entry.BindMask,
/*storageDefined=*/true);
if (entry.HasLastDesc && std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0) return;
if (!entry.Published) {
if (!MGPipeHandleIsPublished(MGPipeKind::Renderbuffer, handle)) {
const MGPResourceDesc createDesc = MGPipeBuildRenderbufferResourceDesc(
renderbuffer, handle, entry.BindMask, /*storageDefined=*/false);
entry.Published = true;
NoteDesc(createDesc, /*isCreate=*/true);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceCreate(createDesc);
PublishCreate(MGPipeKind::Renderbuffer, handle, entry, createDesc);
}
entry.LastDesc = desc;
entry.HasLastDesc = true;
NoteDesc(desc, /*isCreate=*/false);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceRespecify(desc, nullptr);
Bool accepted = ApplyRespecify(desc);
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
if (!accepted) {
// See the texture twin: the applier's refusal is the only thing that can
// say "I hold no record for this handle" once the latch has been set.
const MGPResourceDesc healDesc = MGPipeBuildRenderbufferResourceDesc(
renderbuffer, handle, entry.BindMask, /*storageDefined=*/false);
NoteDesc(healDesc, /*isCreate=*/true);
PublishCreate(MGPipeKind::Renderbuffer, handle, entry, healDesc);
accepted = ApplyRespecify(desc);
}
}
NoteRespecified(entry, desc, accepted);
}
// ---- the drain list (D-D4) ----
@@ -697,11 +739,27 @@ namespace MobileGL::MG_Pipe {
// The per-slot key list is a short linear scan rather than a hash: a level count is
// ~15, the cap on the rect list behind it is 96, and this runs on the glTexSubImage
// path which has just memcpy'd texels.
void NoteLevelDirty(ITextureObject& texture, MobileGL::TextureUploadTarget uploadTarget, Uint level) {
// THE PARAMETER TYPES ARE THE CONTRACT'S (PipeMutation.h): Uint32 rather than
// MobileGL::TextureUploadTarget and Uint, because that declaration is the one door
// MG_State has into the client and it may not name a frontend enumeration.
//
// THERE IS NO CLEAN ARM, and that is a DECLARED DEVIATION rather than a dropped half.
// v1 carried a second entry point for MarkStorageDirty(..., false); the contract's hook
// has no `dirty` parameter, and asking A to widen it would put a second signature in
// MG_Pipe/PipeMutation.h for something the drain already collects. A level that goes
// clean stays on the list until the NEXT drain walks it, where
// `!mipmap->IsStorageDirty(...)` is the first test EmitOneLevel makes and returns
// "nothing owed", so the entry is dropped from both lists there. The cost is one
// IsStorageDirty call per cleaned level per drain, the list is bounded by the (texture,
// level) pairs dirtied since the last validate point, and a re-dirty before that drain
// is already covered by the entry still standing. What it must NOT be confused with is
// dropping the level's TEXELS: nothing here clears a dirty flag.
void NoteLevelDirty(ITextureObject& texture, Uint32 uploadTarget, Uint32 level) {
if (m_draining) return;
const MGPipeHandle handle = AcquireTexture(texture.GetLifetimeId(), &texture);
Entry& entry = EntryFor(m_textures, handle);
const Uint32 key = PackLevelKey(uploadTarget, level);
const Uint32 key = PackLevelKey(static_cast<MobileGL::TextureUploadTarget>(uploadTarget),
static_cast<Uint>(level));
for (const Uint32 present : entry.DrainKeys) {
if (present == key) return;
}
@@ -709,26 +767,6 @@ namespace MobileGL::MG_Pipe {
m_drain.push_back(DrainEntry{handle, key});
}
// MarkStorageDirty(..., false) from outside the drain - a level respecified, truncated
// or explicitly marked clean. The entry stops describing anything and is dropped from
// the per-slot list; the global list is compacted at the next drain, which is where
// walking it is already paid for.
void NoteLevelClean(ITextureObject& texture, MobileGL::TextureUploadTarget uploadTarget, Uint level) {
if (m_draining) return;
const MGPipeHandle handle = FindTexture(texture);
if (MGPipeHandleIsNull(handle)) return;
const SizeT slot = handle.Slot;
if (slot >= m_textures.size()) return;
Entry& entry = m_textures[slot];
const Uint32 key = PackLevelKey(uploadTarget, level);
for (SizeT i = 0; i < entry.DrainKeys.size(); ++i) {
if (entry.DrainKeys[i] != key) continue;
entry.DrainKeys[i] = entry.DrainKeys.back();
entry.DrainKeys.pop_back();
return;
}
}
// The DRAIN, at the validate point: one resource_subdata per dirty (storage owner,
// upload target, level).
//
@@ -780,6 +818,20 @@ namespace MobileGL::MG_Pipe {
Uint64 RespecifyCount() const { return m_respecifies; }
Uint64 ParamCount() const { return m_paramSets; }
Uint64 SubDataCount() const { return m_subDatas; }
// Records the applier REFUSED. The dirty flag survives one of these, which is the whole
// of D-D5 step 1 - so a case that wants to prove the flag survived asserts on this.
Uint64 RefusedSubDataCount() const { return m_refusedSubDatas; }
// What create_sampler_state put on the wire on this emitter's behalf, so the csob-blob
// accounting does not under-report 100 bytes per built-in sampler mint. set_texture_params
// itself returns no byte count - it is not emitted from the validate point's payload
// histogram - so this is where the cache's answer lands.
Uint64 SamplerCsoPayloadBytes() const { return m_samplerCsoPayloadBytes; }
MGPipeHandle BuiltinSamplerOf(MGPipeHandle handle) const {
const SizeT slot = handle.Slot;
if (MGPipeHandleIsNull(handle) || slot >= m_textures.size()) return kMGPipeNullHandle;
const Entry& entry = m_textures[slot];
return entry.Gen == handle.Gen ? entry.BuiltinSampler : kMGPipeNullHandle;
}
SizeT DrainListSize() const { return m_drain.size(); }
// A fresh context: what the server has is no longer what this emitter last sent. Only
@@ -792,25 +844,24 @@ namespace MobileGL::MG_Pipe {
// uploads a context switch has not flushed yet.
void Reset() {}
void ResetCounters() { m_creates = m_respecifies = m_paramSets = m_subDatas = 0; }
// ---- the arm (see kMGPipeWiredTextureSubsystem) ----
//
// "Does this build's texture family emit at all", initialised from the wired constant.
// It is a RUNTIME latch and not a constant only because the flip is blocked on the wire
// package's `w1` while the conversion below is finished: a unit case arms it, drives a
// frontend mutation and asserts on the record the emitter built, so the conversion is
// gated by a test on a base whose applier could not yet hold that record. Once the
// constant is flipped this stays true for the life of the process and ArmForTest is
// redundant rather than wrong.
Bool Armed() const { return m_armed; }
void ArmForTest(Bool armed) { m_armed = armed; }
void ResetCounters() {
m_creates = m_respecifies = m_paramSets = m_subDatas = 0;
m_refusedSubDatas = 0;
m_samplerCsoPayloadBytes = 0;
}
// A unit fixture's per-case reset; the library never calls it. See
// MGPipeResourceTracker::ResetForTest for the rule this restates: a texture handle and
// the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
// per-context and no re-publication path exists or may exist.
void ResetForTest() {
// EVERY REFERENCE THIS EMITTER OWES IS GIVEN BACK FIRST. A case that dropped the
// table without releasing would pin cache entries for the rest of the process and
// the next case's LRU would mint over capacity for reasons it cannot see.
for (Entry& entry : m_textures) {
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
entry.BuiltinSampler = kMGPipeNullHandle;
}
m_textures.clear();
m_renderbuffers.clear();
m_drain.clear();
@@ -819,7 +870,6 @@ namespace MobileGL::MG_Pipe {
m_lastDesc = MGPResourceDesc{};
m_lastParams = MGPTextureParams{};
m_lastSubData = MGPSubData{};
m_armed = (kMGPipeWiredTextureSubsystem & kMGPipeSubsystemTextureResources) != 0;
ResetCounters();
}
@@ -828,9 +878,19 @@ namespace MobileGL::MG_Pipe {
ITextureObject* Texture = nullptr;
Uint32 Gen = 0;
Uint16 BindMask = 0;
Bool Published = false;
Bool ForceParamsResync = false;
Bool HasLastDesc = false;
// ID-14/ID-17: the CSO C's content-addressed cache handed this texture's BUILT-IN
// sampler, and the ONE reference this emitter owes a Release for. Null until the
// first set_texture_params. There is no Published flag beside it: c0b's
// {kind, slot, gen} latch is the one answer both halves read.
MGPipeHandle BuiltinSampler{};
// The version-first skip for set_texture_params, and it reads BOTH counters
// (clientsp-v2 rule 4): glTexParameter* moves GetTextureParamsVersion(), a write
// that lands on the built-in SamplerObject moves only SamplerObject::GetVersion().
Bool HasParamsLatch = false;
Uint16 ParamsVersion = 0;
Uint16 SamplerVersion = 0;
MGPResourceDesc LastDesc{};
Vector<Uint32> DrainKeys;
};
@@ -857,15 +917,72 @@ namespace MobileGL::MG_Pipe {
const SizeT slot = handle.Slot;
return slot < table.size() ? table[slot].BindMask : Uint16{0};
}
static Bool PublishedIn(const Vector<Entry>& table, MGPipeHandle handle) {
if (MGPipeHandleIsNull(handle)) return false;
const SizeT slot = handle.Slot;
return slot < table.size() && table[slot].Published && table[slot].Gen == handle.Gen;
// A SLOT THE ALLOCATOR HAS HANDED OUT AGAIN CARRIES ITS PREDECESSOR'S ENTRY, and every
// field in it is a lie about the new object (m4). The sticky BindMask is the one that
// bites: the framebuffer emitter ORs RENDER_TARGET / DEPTH_STENCIL into these entries
// whether or not the texture family is on, so a recycled slot's new texture inherited
// the dead one's mask and its first descriptor said so. The generation is what
// distinguishes them and the reset is here because AcquireTexture is the one door.
//
// IT IS ALSO THE ONLY MOMENT THIS PACKAGE CAN SEE A TEXTURE DIE. The death helper is
// A's (MGPipeEmitTextureDestroyAndFree) and does not forward to this emitter, so the
// built-in sampler's cache reference is dropped here - bounded by the number of live
// texture slots rather than unbounded, which is the shape ID-17 rule 3 names.
void RetireIfRecycled(Entry& entry, MGPipeHandle handle) {
if (entry.Gen == handle.Gen) return;
MGPipeSamplerCsoCacheInstance().Release(entry.BuiltinSampler);
entry = Entry{};
}
static void Retire(Vector<Entry>& table, MGPipeHandle handle) {
const SizeT slot = handle.Slot;
if (slot >= table.size() || table[slot].Gen != handle.Gen) return;
table[slot] = Entry{};
// resource_create, and the LATCH IS TAKEN ONLY WHERE THE CREATE ACTUALLY WENT OUT
// (D-I1, c0b): MGPipeHandleIsPublished is what the death helper reads, so latching on
// a call the applier refused would emit a resource_destroy for a record that does not
// exist - a refused call the applier asserts on in a verify build.
void PublishCreate(MGPipeKind kind, MGPipeHandle handle, Entry& entry,
const MGPResourceDesc& desc) {
Bool accepted = false;
Bool dispatched = false;
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
dispatched = true;
accepted = MGPipeApplyResourceCreate(desc);
}
if (dispatched && !accepted) return;
MGPipeNoteHandlePublished(kind, handle);
entry.LastDesc = desc;
entry.HasLastDesc = true;
}
static Bool ApplyRespecify(const MGPResourceDesc& desc) {
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
return MGPipeApplyResourceRespecify(desc, nullptr);
}
return false;
}
static void NoteRespecified(Entry& entry, const MGPResourceDesc& desc, Bool accepted) {
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
if (!accepted) return;
}
entry.LastDesc = desc;
entry.HasLastDesc = true;
}
// THE METADATA RESPECIFY (ID-18 M4). Every storage-defining field is the stored
// descriptor's own byte for byte - the record IS entry.LastDesc with a new mask - which
// is what makes the applier classify it as a metadata update: the descriptor is
// replaced so the mask and the hint take their new values, the serial advances, and no
// pending upload is dropped.
void RepublishMask(MGPipeKind kind, MGPipeHandle handle, Entry& entry) {
if (!MGPipeTextureSubsystemEnabled()) return;
// Nothing has described this object to the applier yet, so the create or the first
// respecify carries the new mask anyway - both read entry.BindMask.
if (!entry.HasLastDesc || !MGPipeHandleIsPublished(kind, handle)) return;
MGPResourceDesc desc = entry.LastDesc;
desc.BindMask = entry.BindMask;
desc.ImageBindableHint = (entry.BindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0;
if (std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0) return;
NoteDesc(desc, /*isCreate=*/false);
NoteRespecified(entry, desc, ApplyRespecify(desc));
}
void NoteDesc(const MGPResourceDesc& desc, Bool isCreate) {
@@ -915,8 +1032,11 @@ namespace MobileGL::MG_Pipe {
m_lastSubData = MGPSubData{};
m_lastSubData.Res = pending.Handle;
// The contract's packer takes two Uint32s (c0c keeps MGPipeTypes.h backend-neutral),
// so the frontend enumeration is widened here rather than there.
m_lastSubData.Target = MGPipePackSubDataTarget(
MGPipeResourceTargetForTextureTarget(texture->GetTarget()), uploadTarget);
static_cast<Uint32>(MGPipeResourceTargetForTextureTarget(texture->GetTarget())),
static_cast<Uint32>(uploadTarget));
m_lastSubData.Level = static_cast<Uint16>(level);
// ALWAYS 1 ON THE CLIENT SIDE. The conversion fallbacks (the packed-norm, widened
// and fallback upload preparers) are the server's and run there, so the bytes this
@@ -936,17 +1056,44 @@ namespace MobileGL::MG_Pipe {
m_lastSubData.Blob.Offset = static_cast<Uint64>(reinterpret_cast<std::uintptr_t>(shadow));
m_lastSubData.Blob.Size = 0;
// THE REGION LIST IS THE CALL'S VARIABLE TAIL AND IT IS HANDED OVER (M1). v1 built
// m_regions, wrote its size into RegionCount and passed nothing, so on this base -
// where the applier's tail exists - every scattered upload would have declared N
// regions and supplied none (the applier faults on exactly that). A null tail is
// correct ONLY for the whole-level shape, where RegionCount is 0.
Bool accepted = false;
Bool dispatched = false;
if constexpr (MGPipeTextureRecordsReachTheApplier()) {
MGPipeApplyResourceSubData(m_lastSubData, shadow);
dispatched = true;
accepted = MGPipeApplyResourceSubData(m_lastSubData, shadow,
m_regions.empty() ? nullptr : m_regions.data());
}
++m_subDatas;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::ClientTextureUploadEmissions, 1);
}
bytes += sizeof(MGPSubData) + m_regions.size() * sizeof(MGPSubRegion);
// THE CLIENT CLEARS ITS OWN FLAG, and only now (D-D5's inversion): the record was
// accepted, the applier holds the shape, and MG_Impl contains no reader of this
// texture's dirty state at all - the frontend never reads it back.
// THE CLIENT CLEARS ITS OWN FLAG ONLY FOR A LEVEL THE APPLIER ACCEPTED (D-D5 step 1
// read literally; ID-18 M3). v1 cleared on DISPATCH - and, with the wired constant
// still 0, even on a call the `if constexpr` had discarded - so any refusal left the
// server with nothing and the client with a clean flag, and since MG_Impl contains
// no reader of a texture's dirty state the level simply stopped updating for the
// life of the texture. The two refusal paths are invisible from here without this
// answer: a dead or stale handle is a counted no-op and a corrupt record is a Fatal
// that deliberately moves no counter.
//
// A REFUSED LEVEL STAYS DIRTY AND STAYS ON THE DRAIN LIST, which is the safe
// direction and self-heals: the ordinary cause is a record the applier does not
// hold, and the next respecify's self-healing create gives it one. It is LOUD
// because a permanently refused level would otherwise re-emit once per verb for
// ever with nothing to show for it.
if (dispatched && !accepted) {
++m_refusedSubDatas;
MGLOG_E_ONCE("MGPipe: resource_subdata for texture {slot=%u, gen=%u} level %u was refused; "
"the level stays dirty and is retried at the next validate point",
pending.Handle.Slot, pending.Handle.Gen, static_cast<Uint>(level));
return false;
}
mipmap->MarkStorageDirty(uploadTarget, level, false);
return true;
}
@@ -965,7 +1112,8 @@ namespace MobileGL::MG_Pipe {
Uint64 m_respecifies = 0;
Uint64 m_paramSets = 0;
Uint64 m_subDatas = 0;
Bool m_armed = (kMGPipeWiredTextureSubsystem & kMGPipeSubsystemTextureResources) != 0;
Uint64 m_refusedSubDatas = 0;
Uint64 m_samplerCsoPayloadBytes = 0;
};
inline MGPipeTextureEmitter& MGPipeTextureEmitterInstance() {
@@ -978,107 +1126,33 @@ namespace MobileGL::MG_Pipe {
}
inline Bool MGPipeTextureSubsystemEnabled() {
return MGPipeTextureEmitterInstance().Armed() &&
return (kMGPipeWiredTextureSubsystem & kMGPipeSubsystemTextureResources) != 0 &&
(MG_Config::Features.PipePush & kMGPipeSubsystemTextureResources) != 0;
}
// ---------------------------------------------------------------------------------
// The six entry points MG_State calls. See this file's header comment for why they are
// here rather than in MG_Pipe/PipeMutation.h.
// WHAT USED TO BE HERE, AND WHY IT IS NOT (c0b, ID-13)
// ---------------------------------------------------------------------------------
// From TextureObjectBase's constructor. The mint is unconditional in a push build; the
// CALL is what the subsystem predicate gates.
inline void MGPipeMintAndCreateTexture(MG_State::GLState::ITextureObject* object, Uint64 lifetimeId,
MobileGL::TextureTarget target, Uint externalIndex) {
MGPipeTextureEmitter& emitter = MGPipeTextureEmitterInstance();
if (!MGPipeTextureSubsystemEnabled()) {
emitter.AcquireTexture(lifetimeId, object);
return;
}
emitter.EmitTextureCreateFromBase(lifetimeId, object, target, externalIndex);
}
inline void MGPipeEmitTextureRespecify(MG_State::GLState::ITextureObject& texture) {
if (!MGPipeTextureSubsystemEnabled()) return;
MGPipeTextureEmitterInstance().EmitTextureRespecify(texture);
}
inline void MGPipeEmitTextureParams(MG_State::GLState::ITextureObject& texture) {
if (!MGPipeTextureSubsystemEnabled()) return;
MGPipeTextureEmitterInstance().EmitTextureParams(texture);
}
inline void MGPipeNoteTextureLevelDirty(MG_State::GLState::ITextureObject& texture,
MobileGL::TextureUploadTarget uploadTarget, Uint level, Bool dirty) {
if (!MGPipeTextureSubsystemEnabled()) return;
MGPipeTextureEmitter& emitter = MGPipeTextureEmitterInstance();
if (dirty) {
emitter.NoteLevelDirty(texture, uploadTarget, level);
} else {
emitter.NoteLevelClean(texture, uploadTarget, level);
}
}
inline void MGPipeMintAndCreateRenderbuffer(MG_State::GLState::RenderbufferObject& renderbuffer) {
MGPipeTextureEmitter& emitter = MGPipeTextureEmitterInstance();
if (!MGPipeTextureSubsystemEnabled()) {
emitter.AcquireRenderbuffer(renderbuffer.GetLifetimeId());
return;
}
emitter.EmitRenderbufferCreate(renderbuffer);
}
inline void MGPipeEmitRenderbufferRespecify(MG_State::GLState::RenderbufferObject& renderbuffer) {
if (!MGPipeTextureSubsystemEnabled()) return;
MGPipeTextureEmitterInstance().EmitRenderbufferRespecify(renderbuffer);
}
// ---------------------------------------------------------------------------------
// STEP 1 OF THE THREE-STEP DEATH ORDER, and it is here rather than inside the contract's
// MGPipeEmitTextureDestroyAndFree for the same ownership reason the six entry points above
// are: MG_Impl/Pipe/PipeFill.cpp, where that helper lives, is the contract package's for
// the whole phase, and at the tag it hard-codes `published = false` with the note "the
// texture emitter publishes nothing yet". This client package cannot edit that line, so it
// supplies the answer from the destructor instead, one statement BEFORE the helper:
//
// ~TextureObjectBase / ~RenderbufferObject
// -> MGPipeEmit<Kind>ResourceDestroy(lifetimeId) // 1. the wire delete
// -> MGPipeEmit<Kind>DestroyAndFree(lifetimeId) // 2. the notice, 3. the slot
// v1 carried eight free functions - MGPipeMintAndCreateTexture, MGPipeEmitTextureRespecify,
// MGPipeEmitTextureParams, MGPipeNoteTextureLevelDirty, MGPipeMintAndCreateRenderbuffer,
// MGPipeEmitRenderbufferRespecify and the two ...ResourceDestroy halves - because at the
// contract tag MG_Pipe/PipeMutation.h declared no texture birth hook and
// MG_Impl/Pipe/PipeFill.cpp's death helpers hard-coded `published = false`. Every one of
// them is now A's:
//
// which is EXACTLY the order PipeMutation.h fixes and not a variation on it: the applier's
// record is dropped while nothing can have re-handed the slot out, the death notice is
// raised while the handle still resolves, and the slot goes back last. The integrator can
// fold these two functions into the helpers' bodies in one mechanical commit once the
// phase's file ownership relaxes.
// * the four MINTS and the nine EMISSIONS are declared in PipeMutation.h and defined in
// PipeFill.cpp, which gates them on FamilyIsLive(bit, kMGPipeWiredTextureSubsystem) and
// forwards to this class through ForwardWhenWired. MGPipeEmitTextureParams in
// particular was a NAME COLLISION - the contract declares that exact signature - so
// keeping the inline definition here would not have compiled at all;
// * step 1 of the death order is inside MGPipeEmitTextureDestroyAndFree /
// ...RenderbufferDestroyAndFree, which read MGPipeHandleIsPublished and emit the
// resource_destroy themselves, so the destructors call ONE helper and not two;
// * the publication latch is PipeFill.cpp's {kind, slot, gen} table, written by
// PublishCreate above and read by those helpers.
//
// PUBLISHED-GATED RATHER THAN SLOT-GATED, for the reason PipeFill.cpp states in full: a
// slot is not evidence of a record, because a backend twin table mints one through
// MGPipeSlots().Acquire whether or not the subsystem ever asked this client to emit a
// create - which is exactly what a MOBILEGL_PIPE_PUSH lane with P4a's bits clear runs - and
// a resource_destroy on such a handle is a refused call the applier counts and asserts on.
inline Bool MGPipeEmitTextureResourceDestroy(Uint64 lifetimeId) {
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Texture, lifetimeId);
MGPipeTextureEmitter& emitter = MGPipeTextureEmitterInstance();
if (!emitter.TextureRecordIsPublished(handle)) return false;
MGPHandleOnly only{};
only.Handle = handle;
only.Kind = static_cast<Uint32>(MGPipeKind::Texture);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceDestroy(only);
emitter.NoteTextureRecordDestroyed(handle);
return true;
}
inline Bool MGPipeEmitRenderbufferResourceDestroy(Uint64 lifetimeId) {
const MGPipeHandle handle = MGPipeSlots().FindByLifetimeId(MGPipeKind::Renderbuffer, lifetimeId);
MGPipeTextureEmitter& emitter = MGPipeTextureEmitterInstance();
if (!emitter.RenderbufferRecordIsPublished(handle)) return false;
MGPHandleOnly only{};
only.Handle = handle;
only.Kind = static_cast<Uint32>(MGPipeKind::Renderbuffer);
if constexpr (MGPipeTextureRecordsReachTheApplier()) MGPipeApplyResourceDestroy(only);
emitter.NoteRenderbufferRecordDestroyed(handle);
return true;
}
// The self-healing create in EmitResourceRespecify stays this file's: c0b provides no such
// path and it is what repairs a texture born while the subsystem bit was clear.
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH
@@ -9,13 +9,10 @@
#include "RenderbufferObject.h"
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_State/GLState/StateObjectDeathNotice.h>
#if MOBILEGL_PIPE_PUSH
// The second and last MG_State translation unit that sees the client's texture emitter. A
// renderbuffer has no base class to hang protected helpers on and exactly one .cpp, so the
// include is the whole coupling; see MG_Impl/Pipe/TextureEmit.h's header comment for why the
// declaration cannot live in MG_Pipe/PipeMutation.h this phase.
#include <MG_Impl/Pipe/TextureEmit.h>
#endif
// The contract's own door, exactly as the texture half takes it (c0b): the four renderbuffer
// hooks this file calls are declared in MG_Pipe/PipeMutation.h and defined in
// MG_Impl/Pipe/PipeFill.cpp, so no MG_State translation unit includes the client's emitter.
#include <MG_Pipe/PipeMutation.h>
#include <atomic>
@@ -39,7 +36,12 @@ namespace MobileGL {
// shape with textures and buffers and nothing else - and its handle is minted
// whatever the subsystem bitmask says, because MGPSurface::Res names it out of the
// framebuffer subsystem.
MG_Pipe::MGPipeMintAndCreateRenderbuffer(*this);
// TWO CALLS AND NOT ONE (c0b): the mint is unconditional in a push build
// because a renderbuffer is named by handle out of the framebuffer subsystem
// whether or not its own family is switched on; the create is what the gate in
// PipeFill.cpp decides.
MG_Pipe::MGPipeMintRenderbufferHandle(*this);
MG_Pipe::MGPipeEmitRenderbufferResourceCreate(*this);
#endif
}
@@ -51,7 +53,8 @@ namespace MobileGL {
// still resolves - and the slot last. Steps 2 and 3 are the contract's helper;
// step 1 is this package's, one statement earlier, because the helper's file
// belongs to the contract package for the whole phase.
MG_Pipe::MGPipeEmitRenderbufferResourceDestroy(m_lifetimeId);
// All three steps are the contract's helper (c0b); v1's separate step-1 call
// is deleted, not kept, for the reason ~TextureObjectBase states in full.
MG_Pipe::MGPipeEmitRenderbufferDestroyAndFree(m_lifetimeId);
}
#endif
@@ -145,7 +148,7 @@ namespace MobileGL {
// The emitter dedupes on the built descriptor, so glRenderbufferStorage's three-setter
// sequence publishes once rather than three times.
void RenderbufferObject::PipePublishDescriptor() {
MG_Pipe::MGPipeEmitRenderbufferRespecify(*this);
MG_Pipe::MGPipeEmitRenderbufferResourceRespecify(*this);
}
#endif
} // namespace GLState
@@ -12,15 +12,13 @@
#include "MG_Util/Types.h"
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Pipe/PipeMutation.h>
#if MOBILEGL_PIPE_PUSH
// THE ONE MG_State TRANSLATION UNIT THAT SEES THE CLIENT'S TEXTURE EMITTER, on purpose: the
// three PipePublish* helpers below are declared on TextureObjectBase and defined here, so the
// cube's, the view's and the buffer texture's translation units call an inherited member and
// still see only a declaration. That is the layering MG_Pipe/PipeMutation.h gives the buffer
// family; P4a cannot use that header because it belongs to the contract package for the whole
// phase and carries no texture row (TextureEmit.h's header comment has the full argument).
#include <MG_Impl/Pipe/TextureEmit.h>
#endif
// NO MG_State TRANSLATION UNIT SEES THE CLIENT'S EMITTER ANY MORE (c0b, ID-13). v1 included
// MG_Impl/Pipe/TextureEmit.h here and in RenderbufferObject.cpp because at the contract tag
// MG_Pipe/PipeMutation.h carried no texture row; it now declares the four mints, the nine
// emissions and the publication latch, so this file sees a DECLARATION exactly as
// BufferObject.cpp does and the closure gate's mutation-header probe has nothing to find.
// The three PipePublish* helpers stay on TextureObjectBase so the cube's, the view's and the
// buffer texture's translation units keep calling an inherited member.
namespace MobileGL {
namespace MG_State {
@@ -53,29 +51,31 @@ namespace MobileGL {
// name and leaves a still-bound object very much alive;
// 3. the slot LAST, and a double free on a stale generation is a proven no-op.
//
// Steps 2 and 3 - plus the SamplerViewCso minted off this same lifetime id - are
// the contract's helper. The BUILT-IN SAMPLER is deliberately not released from
// here: it is a real SamplerObject with its own lifetime id and its own
// destructor, which runs immediately after this body and takes the same helper
// shape. Step 1 is the client package's, one statement earlier, because the
// helper's file belongs to the contract package for the whole phase.
MG_Pipe::MGPipeEmitTextureResourceDestroy(m_lifetimeId);
// ALL THREE STEPS ARE THE CONTRACT'S HELPER (c0b): it reads the publication
// latch, emits the resource_destroy itself, raises the notice while the handle
// still resolves and frees the slot last. v1 emitted step 1 from a second
// statement here because at the tag the helper hard-coded `published = false`;
// that statement is deleted rather than kept, since a second delete for a
// record the helper has already dropped is a refused call the applier asserts
// on. The SamplerViewCso minted off this same lifetime id goes with it. The
// BUILT-IN SAMPLER does not: it is a real SamplerObject with its own lifetime
// id and its own destructor, which takes the same helper shape.
MG_Pipe::MGPipeEmitTextureDestroyAndFree(m_lifetimeId);
}
// ---- P4a's three client emission points (see TextureObject.h) ----
void TextureObjectBase::PipePublishDescriptor() {
MG_Pipe::MGPipeEmitTextureRespecify(*this);
MG_Pipe::MGPipeEmitTextureResourceRespecify(*this);
}
void TextureObjectBase::PipePublishParams() {
MG_Pipe::MGPipeEmitTextureParams(*this);
}
void TextureObjectBase::PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel,
Bool dirty) {
MG_Pipe::MGPipeNoteTextureLevelDirty(*this, uploadTarget, mipmapLevel, dirty);
void TextureObjectBase::PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel) {
MG_Pipe::MGPipeNoteTextureLevelDirty(*this, static_cast<Uint32>(uploadTarget),
static_cast<Uint32>(mipmapLevel));
}
#endif
@@ -115,15 +115,25 @@ namespace MobileGL {
// because set_framebuffer_state and set_sampler_views name this texture by handle
// out of two different subsystems.
//
// NOTHING VIRTUAL IS TOUCHED HERE and that is a correctness requirement rather
// than a style: the derived object does not exist yet, and
// ITextureObject::GetStorageType is PURE - calling it from a base constructor is
// undefined behaviour. The target and the GL name are the two facts a create
// carries and both are plain members by this point; the storage kind is derived
// from the target, which is exact (TextureObjectBuffer is the only class that
// reports Buffer and TextureBuffer is the only target it is constructed with).
MG_Pipe::MGPipeMintAndCreateTexture(static_cast<ITextureObject*>(this), m_lifetimeId, target,
externalIndex);
// NOTHING THE DERIVED CLASS IMPLEMENTS IS TOUCHED HERE and that is a
// correctness requirement rather than a style: the derived object does not exist
// yet, so ITextureObject::GetStorageType and ::GetUploadTargets - PURE, with no
// body on this base - would be undefined behaviour. The emitter reads GetTarget()
// and GetExternalIndex(), which TextureObjectBase itself overrides and which
// therefore dispatch to this class's own bodies over members the mem-init list
// has already written; the storage kind is derived from the target, which is
// exact (TextureObjectBuffer is the only class that reports Buffer and
// TextureBuffer is the only target it is constructed with).
//
// TWO CALLS AND NOT ONE (c0b): the MINT is unconditional in a push build -
// set_framebuffer_state and set_sampler_views name this texture by handle out of
// two other subsystems, so gating it would make them emit null handles in exactly
// the A/B arm that exists to isolate the families - and the CREATE is what the
// subsystem gate in PipeFill.cpp decides.
(void)target;
(void)externalIndex;
MG_Pipe::MGPipeMintTextureHandle(*this);
MG_Pipe::MGPipeEmitTextureResourceCreate(*this);
#endif
}
@@ -498,7 +508,9 @@ namespace MobileGL {
}
m_textureStorage.MarkDirty(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, dirty);
#if MOBILEGL_PIPE_PUSH
PipeNoteLevelDirty(uploadTarget, mipmapLevel, dirty);
// THE DRAIN LIST HAS NO CLEAN ARM (see TextureEmit.h): a level that goes clean
// is collected at the next drain, whose first test is IsStorageDirty.
if (dirty) PipeNoteLevelDirty(uploadTarget, mipmapLevel);
#endif
}
@@ -517,7 +529,7 @@ namespace MobileGL {
// forwards this call to the OWNER's method after remapping the level and the
// region, so an upload through a view and an upload through the owner arrive here
// on the same object with the same owner-side coordinates.
PipeNoteLevelDirty(uploadTarget, mipmapLevel, true);
PipeNoteLevelDirty(uploadTarget, mipmapLevel);
#endif
}
@@ -215,13 +215,11 @@ namespace MobileGL::MG_State::GLState {
// that reaches the pull build is inside this guard, so the pull build's symbol set is
// byte-for-byte the one it had before the phase.
//
// DECLARED HERE AND DEFINED IN TextureObject.cpp, which is the ONE MG_State
// translation unit that includes the client's MG_Impl/Pipe/TextureEmit.h. Every other
// texture .cpp - the cube's, the view's, the buffer texture's - calls the inherited
// helper and still sees only a declaration, which is the same layering
// MG_Pipe/PipeMutation.h gives the buffer family (that header is the contract
// package's for the whole phase and carries no texture row, which is why the
// declaration lives here instead; see TextureEmit.h's header comment).
// DECLARED HERE AND DEFINED IN TextureObject.cpp, which calls the contract's own hooks
// in MG_Pipe/PipeMutation.h - the same door BufferObject.cpp uses, and no MG_State
// translation unit sees MG_Impl/Pipe/TextureEmit.h at all (c0b, ID-13). They stay
// members rather than free calls so the cube's, the view's and the buffer texture's
// translation units keep calling an inherited helper.
//
// resource_respecify. Called from BumpShapeVersion and from the three parameter
// setters that move a DESCRIPTOR field without moving the shape (immutable levels,
@@ -230,9 +228,11 @@ namespace MobileGL::MG_State::GLState {
void PipePublishDescriptor();
// set_texture_params, from every mutator that bumps m_textureParamsVersion.
void PipePublishParams();
// The sub-data DRAIN LIST. `dirty` false is a level going clean - a respecify, a
// truncation, or the emitter's own clear after an accepted record.
void PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel, Bool dirty);
// The sub-data DRAIN LIST's append, on a level's first dirty mark. There is no clean
// arm: the contract's hook (MG_Pipe/PipeMutation.h) carries no `dirty` flag, and a
// level that goes clean is collected at the next drain, where !IsStorageDirty is the
// first test EmitOneLevel makes.
void PipeNoteLevelDirty(TextureUploadTarget uploadTarget, Uint mipmapLevel);
#endif
const Uint m_externalIndex;
@@ -65,7 +65,7 @@ namespace MobileGL {
#if MOBILEGL_PIPE_PUSH
// SIX FACES, SIX BLOBS, SIX DRAIN KEYS: the upload target is the face, and it is
// what the sub-data record's Target byte carries beside the resource target.
PipeNoteLevelDirty(uploadTarget, mipmapLevel, dirty);
if (dirty) PipeNoteLevelDirty(uploadTarget, mipmapLevel);
#endif
}
@@ -80,7 +80,7 @@ namespace MobileGL {
m_textureStorage.MarkDirtyRegion(GetIndexOfTextureUploadTarget(uploadTarget), mipmapLevel, offset,
size);
#if MOBILEGL_PIPE_PUSH
PipeNoteLevelDirty(uploadTarget, mipmapLevel, true);
PipeNoteLevelDirty(uploadTarget, mipmapLevel);
#endif
}