diff --git a/MobileGL/MG_Impl/Pipe/FramebufferEmit.h b/MobileGL/MG_Impl/Pipe/FramebufferEmit.h index 45a4f325..90f4b5e2 100644 --- a/MobileGL/MG_Impl/Pipe/FramebufferEmit.h +++ b/MobileGL/MG_Impl/Pipe/FramebufferEmit.h @@ -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(MGPipeKind::None); - inline constexpr Uint8 kMGPipeSurfaceKindTexture = static_cast(MGPipeKind::Texture); - inline constexpr Uint8 kMGPipeSurfaceKindRenderbuffer = static_cast(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(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(std::max(attachment.GetTextureLevel(), 0)); surface.Layer = static_cast(std::max(attachment.GetTextureLayer(), 0)); surface.UploadTarget = static_cast(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(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(buffer) - static_cast(FramebufferAttachmentType::Color0) < + static_cast(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(MGPipeFramebufferTarget::Read)) { + if (state.Target == static_cast(MGPipeFramebufferTarget::Named)) { + m_lastNamed = state; + } else if (state.Target == static_cast(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(tokens[i]) - + static_cast(FramebufferAttachmentType::Color0), + static_cast(i), static_cast(kMGPipeMaxColorAttachments)); + ++m_refusals; + return false; + } + } + out = MGPFramebufferState{}; out.Fbo = HandleFor(fbo); out.Target = static_cast(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 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 m_named; MGPFramebufferState m_lastDraw{}; MGPFramebufferState m_lastRead{}; + MGPFramebufferState m_lastNamed{}; Uint64 m_emissions = 0; Uint64 m_refusals = 0; }; diff --git a/MobileGL/MG_Impl/Pipe/TextureEmit.h b/MobileGL/MG_Impl/Pipe/TextureEmit.h index 8a45fd21..dd4e5fb7 100644 --- a/MobileGL/MG_Impl/Pipe/TextureEmit.h +++ b/MobileGL/MG_Impl/Pipe/TextureEmit.h @@ -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. 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 #include #include #include @@ -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((resourceTarget & 0xFFu) | - ((static_cast(uploadTarget) & 0xFFu) << 8)); - } - inline constexpr Uint8 MGPipeSubDataResourceTargetOf(Uint16 packed) { - return static_cast(packed & 0xFFu); - } - inline constexpr Uint8 MGPipeSubDataUploadTargetOf(Uint16 packed) { - return static_cast((packed >> 8) & 0xFFu); - } - static_assert(MGPipePackSubDataTarget(kMGPipeResourceTargetBuffer, - static_cast(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(before | bit); + const Uint16 now = static_cast(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(entry.BindMask | bit); + const Uint16 before = entry.BindMask; + const Uint16 now = static_cast(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(MGPipeResourceTargetForTextureTarget(target)); - desc.StorageKind = MGPipeTextureStorageKindForTarget(target); + desc.Target = static_cast(MGPipeResourceTargetForTextureTarget(texture.GetTarget())); + desc.StorageKind = MGPipeTextureStorageKindForTarget(texture.GetTarget()); desc.BindMask = entry.BindMask; - desc.GlNameForDiag = static_cast(externalIndex); - entry.Published = true; - entry.LastDesc = desc; - entry.HasLastDesc = true; + desc.ImageBindableHint = (entry.BindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0; + desc.GlNameForDiag = static_cast(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(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(uploadTarget), + static_cast(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 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& 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& 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(MGPipeResourceTargetForTextureTarget(texture->GetTarget())), + static_cast(uploadTarget)); m_lastSubData.Level = static_cast(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(reinterpret_cast(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(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 - // -> MGPipeEmitResourceDestroy(lifetimeId) // 1. the wire delete - // -> MGPipeEmitDestroyAndFree(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(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(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 diff --git a/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferObject.cpp b/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferObject.cpp index 31f7149f..21689f25 100644 --- a/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferObject.cpp +++ b/MobileGL/MG_State/GLState/RenderbufferState/RenderbufferObject.cpp @@ -9,13 +9,10 @@ #include "RenderbufferObject.h" #include #include -#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 -#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 #include @@ -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 diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp index 88bc69d7..5626a273 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.cpp @@ -12,15 +12,13 @@ #include "MG_Util/Types.h" #include #include -#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 -#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(uploadTarget), + static_cast(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(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 } diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject.h b/MobileGL/MG_State/GLState/TextureState/TextureObject.h index db06f363..bd3a2d1e 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject.h @@ -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; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp index afabc1eb..00c392fd 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp @@ -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 }