From 33f5e878e732a0e048396510ea72f2bcf0fcedc2 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 8 Sep 2026 12:40:37 -0400 Subject: [PATCH] [Refactor] (Espryt): key the texture, renderbuffer, framebuffer, sampler and program twins on the client-minted handle instead of the frontend object --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 253 ++++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/Managers.h | 174 ++++++++++++++ 2 files changed, 427 insertions(+) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index c8e28d95..49e8c053 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -214,6 +214,26 @@ namespace MobileGL::MG_Backend::DirectGLES { case MG_Pipe::MGPipeKind::ShaderCso: PrgramImpl::g_backendProgramObjects.DestroyByLifetimeId(lifetimeId); break; +#if MOBILEGL_PIPE_PUSH + case MG_Pipe::MGPipeKind::SamplerViewCso: + // P4a (D-I1, D5): the sixth kind, and the only one whose notice names a + // lifetime id that belongs to ANOTHER object - a sampler view is minted off its + // texture's id, one per ITextureObject, so ~TextureObjectBase raises this arm + // and the texture arm above from the same id. That is legal precisely because + // the two kinds have separate slot spaces, and it is why the two arms are + // written out separately rather than folded: the allocator resolves + // (kind, lifetimeId), so each finds its own slot or nothing. + // + // IDEMPOTENT, like every arm here. This is the REDUNDANT SECOND PATH: the + // client's MGPipeEmitSamplerViewCsoDestroyAndFree emits delete_sampler_view, + // raises this notice and frees the slot, in that order. Whichever of the two + // frees first wins; the other resolves nothing, because the allocator erases + // its lifetimeId -> slot mapping on Free and OnFrontendObjectDestroyed answers + // false for a handle it cannot resolve. This twin owns no driver id at all, so + // even a double release is a pointer reset. + SamplerViewImpl::BackendSamplerViewTable::OnFrontendObjectDestroyed(lifetimeId); + break; +#endif case MG_Pipe::MGPipeKind::VertexElementsCso: // P3a C-1: this is now the SECOND path, not the only one. The client speaks the // whole death itself (MGPipeEmitVertexElementsDestroyAndFree: delete the @@ -3484,6 +3504,239 @@ namespace MobileGL::MG_Backend::DirectGLES { void UploadRingOnPresent() { RingOnPresent(g_uploadRing); } } // namespace BufferImpl +#if MOBILEGL_PIPE_PUSH + // ---- P4a (D-K3): the four family arm resolvers, beside BufferImpl's two ---- + // + // They reuse BufferImpl's ClassifyPipeSubsystemArm / StopOnArmlessPipeSubsystem unchanged: + // one voice for "the operator left no arm at all", so the six families cannot disagree + // about what an armless verdict means or about whether it stops. + // + // Every one of the four answers legacyArmSurvivesLegacyMemos = FALSE, because every one of + // the four pre-handle arms is compiled under MOBILEGL_PIPE_LEGACY_MEMOS (Managers.h names + // them per family). So all four can reach NoArm and all four stop with the named + // Fatal{PipeLegacyMemosDisabled, ...} rather than running the very arm the operator asked + // to have taken away and handing back a result measured on it. + namespace { + // Shared by the three dependent families, so the refusal reads the same way three times + // and a future fourth cannot invent a different wording. Returns true when the + // dependency is missing, having said so once. + Bool PipeSubsystemDependencyMissing(Uint64 mask, Uint64 dependencyBit, const char* what) { + if ((mask & dependencyBit) != 0) return false; + MGLOG_E("MGPipe: %s - REFUSING the dependent bit and running the legacy arm. " + "Set both bits, or clear both", + what); + return true; + } + + // The release-build VOICE for StateBackendObjectRegistry::GetOrCreateByHandle's two + // silent refusals, shared by the five kinds P4a re-keys so the wording cannot drift + // between them. It is the shape GetOrCreateBufferResourceForHandle gives P3a's buffer + // family, lifted into one template because five copies of it is five chances to write + // one of them differently. + // + // WHY A VOICE AT ALL: the table asserts, and MOBILEGL_ASSERT compiles out at INFO - + // which all three gate builds and every shipped build are - so an object that silently + // stops being twinned is exactly the failure mode the refusal exists to replace. + // + // Returns null on the legacy arm too, so a caller that reaches this without checking + // its family's arm gets nothing rather than a twin the legacy arm would never see. + template + typename Registry::BackendPtr* AdoptTwinByHandle(Registry& registry, MG_Pipe::MGPipeHandle handle, + const char* kindName) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; + if (handle.Slot >= Registry::SlotTable::kMaxHandleSlot) { + MGLOG_E_ONCE("MGPipe: %s handle slot %u is past the backend table's %u bound - " + "refusing to twin it", + kindName, handle.Slot, Registry::SlotTable::kMaxHandleSlot); + return nullptr; + } + const Uint32 liveGen = registry.LiveGenAt(handle.Slot); + if (liveGen != 0 && liveGen > handle.Gen) { + // SlotTables.h:301-321: forward is a recycle and resets the twin, BACKWARD is + // refused, because adopting it would release the incumbent LIVE twin's driver + // id and then stamp the slot back to the dead object's generation, after which + // the incumbent's own FindByHandle refuses it and it is silently handed a + // fresh, empty twin - a leak AND an object that loses its storage with no + // diagnostic. That is the shape commit d7655247 fixed on the buffer side. + MGLOG_E_ONCE("MGPipe: %s handle {%u, %u} names a generation BEHIND the live " + "twin's %u - refusing rather than dropping the incumbent's driver " + "object", + kindName, handle.Slot, handle.Gen, liveGen); + return nullptr; + } + return registry.GetOrCreateByHandle(handle); + } + } // namespace + + Bool ResolveFramebufferSubsystemArm() { + const Uint64 mask = MG_Config::Features.PipePush; + const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemFramebuffer) != 0; + Bool refused = false; + if (bitSet) { + // BIT 9 REQUIRES BIT 10. Every MGPSurface::Res in the record names a Texture or a + // Renderbuffer handle, and only bit 10 puts twins in those two slot tables; without + // it every attachment lookup would miss and the walk would leave the driver + // framebuffer holding whatever the last owner attached. The mirror pair (bit 10 set, + // bit 9 clear) is FINE: the legacy FBO sync reaches the texture twin through + // SyncTextureObjectToBackend, which dispatches to the handle arm by itself. + refused = PipeSubsystemDependencyMissing( + mask, MG_Pipe::kMGPipeSubsystemTextureResources, + "kMGPipeSubsystemFramebuffer (bit 9) is set but kMGPipeSubsystemTextureResources " + "(bit 10) is clear; MGPSurface::Res names a Texture or Renderbuffer handle and " + "only bit 10 populates those slot tables"); + // D-C3: the wire array is Color[8] and this is the driver's RAW ES cap, which is + // NOT clamped to 8 on the GLES path (ValidateColorAttachmentInRange rejects at or + // above it, and BackendObject_DirectGLES publishes it verbatim). Every campaign + // device reports 8 and ES 3.2's minimum is 8 - but a driver reporting more would + // make the record silently truncate, and truncating silently is the bug class this + // phase is closing, while widening the payload is a wire change nobody has evidence + // for. So: refuse the bit, name the cap, run the legacy arm. + if (!refused && + static_cast(std::max(g_GLESCapabilities.MaxColorAttachments, 0)) > + MG_Pipe::kMGPipeMaxColorAttachments) { + MGLOG_E("MGPipe: this driver reports GL_MAX_COLOR_ATTACHMENTS = %d, above " + "MGPFramebufferState::Color[%u]'s wire width - REFUSING " + "kMGPipeSubsystemFramebuffer (bit 9) rather than truncating the record, " + "and running the legacy framebuffer arm", + g_GLESCapabilities.MaxColorAttachments, MG_Pipe::kMGPipeMaxColorAttachments); + refused = true; + } + } + const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm( + bitSet && !refused, MG_Config::Features.PipeLegacyMemos, + /*legacyArmSurvivesLegacyMemos=*/false); + if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) { + BufferImpl::StopOnArmlessPipeSubsystem( + "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemFramebuffer (bit 9) clear (or refuses " + "it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle g_fboSynced* arm"); + } + const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles; + MGLOG_D("MGPipe: Espryt framebuffer family runs the %s arm", enabled ? "handle" : "legacy"); + return enabled; + } + + Bool ResolveTextureResourceSubsystemArm() { + const Uint64 mask = MG_Config::Features.PipePush; + const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemTextureResources) != 0; + Bool refused = false; + if (bitSet) { + // BIT 10 REQUIRES BIT 7. A buffer texture's MGPResourceDesc::BufferForTexBuffer + // names a Buffer handle and only bit 7 puts twins in the resource slot table, so + // with bit 7 clear every glTexBuffer would resolve to no storage at all. The mirror + // pair (bit 7 set, bit 10 clear) is FINE and is P3a's shipped configuration. + refused = PipeSubsystemDependencyMissing( + mask, MG_Pipe::kMGPipeSubsystemResources, + "kMGPipeSubsystemTextureResources (bit 10) is set but kMGPipeSubsystemResources " + "(bit 7) is clear; a buffer texture's BufferForTexBuffer names a Buffer handle " + "and only bit 7 populates the resource slot table"); + } + const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm( + bitSet && !refused, MG_Config::Features.PipeLegacyMemos, + /*legacyArmSurvivesLegacyMemos=*/false); + if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) { + BufferImpl::StopOnArmlessPipeSubsystem( + "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemTextureResources (bit 10) clear (or " + "refuses it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables the pre-handle texture " + "cheap-gate trio"); + } + const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles; + MGLOG_D("MGPipe: Espryt texture-resource family runs the %s arm", enabled ? "handle" : "legacy"); + return enabled; + } + + Bool ResolveSamplerSubsystemArm() { + const Uint64 mask = MG_Config::Features.PipePush; + const Bool bitSet = (mask & MG_Pipe::kMGPipeSubsystemSamplers) != 0; + Bool refused = false; + if (bitSet) { + // BIT 11 REQUIRES BIT 10, and this is the pair G12 drives at 0x9ff. Every + // MGPBoundView::Texture and every MGPImageView::Res names a Texture handle, and + // only bit 10 puts one in the slot table; without it every per-unit lookup would + // miss and the walk would `continue` WITHOUT unbinding - i.e. every draw would + // sample through whatever the unit last held, which is exactly the shape the + // bit-8-without-bit-7 refusal exists to prevent one family over. The mirror pair + // (bit 10 set, bit 11 clear) is FINE. + refused = PipeSubsystemDependencyMissing( + mask, MG_Pipe::kMGPipeSubsystemTextureResources, + "kMGPipeSubsystemSamplers (bit 11) is set but kMGPipeSubsystemTextureResources " + "(bit 10) is clear; every MGPBoundView::Texture and MGPImageView::Res names a " + "Texture handle and only bit 10 populates that slot table"); + } + const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm( + bitSet && !refused, MG_Config::Features.PipeLegacyMemos, + /*legacyArmSurvivesLegacyMemos=*/false); + if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) { + BufferImpl::StopOnArmlessPipeSubsystem( + "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemSamplers (bit 11) clear (or refuses " + "it) and MOBILEGL_PIPE_LEGACY_MEMOS=0 disables UnitSamplerLookupMemo's WeakPtr " + "arm and SamplerPassMemo's raw-pointer rows"); + } + const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles; + MGLOG_D("MGPipe: Espryt sampler family runs the %s arm", enabled ? "handle" : "legacy"); + return enabled; + } + + Bool ResolveProgramSubsystemArm() { + // Bit 12 depends on NOTHING, and that is said out loud rather than left as an absence: + // a ShaderCso handle names no texture and no buffer, the archive rides beside the + // record as a companion pointer, and the eight extra inputs the server still + // specialises on (D-H5) are read from state this backend already holds. + const Bool bitSet = (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemPrograms) != 0; + const BufferImpl::PipeSubsystemArmVerdict verdict = BufferImpl::ClassifyPipeSubsystemArm( + bitSet, MG_Config::Features.PipeLegacyMemos, /*legacyArmSurvivesLegacyMemos=*/false); + if (verdict == BufferImpl::PipeSubsystemArmVerdict::NoArm) { + BufferImpl::StopOnArmlessPipeSubsystem( + "MOBILEGL_PIPE_PUSH leaves kMGPipeSubsystemPrograms (bit 12) clear and " + "MOBILEGL_PIPE_LEGACY_MEMOS=0 disables g_programTwinLookupMemo"); + } + const Bool enabled = verdict == BufferImpl::PipeSubsystemArmVerdict::Handles; + MGLOG_D("MGPipe: Espryt program family runs the %s arm", enabled ? "handle" : "legacy"); + return enabled; + } + + namespace SamplerViewImpl { + BackendSamplerViewTable g_backendSamplerViews; + + BackendSamplerViewObject* GetOrCreateSamplerViewForHandle(MG_Pipe::MGPipeHandle view) { + if (MG_Pipe::MGPipeHandleIsNull(view)) return nullptr; + // The table refuses both of these itself; this is the release-build VOICE for the + // refusal, because MOBILEGL_ASSERT compiles out at INFO and a view that silently + // stops being twinned is the failure mode the refusal exists to replace. Same shape + // as GetOrCreateBufferResourceForHandle's. + if (view.Slot >= BackendSamplerViewTable::kMaxHandleSlot) { + MGLOG_E_ONCE("MGPipe: sampler-view handle slot %u is past the backend table's %u " + "bound - refusing to twin it", + view.Slot, BackendSamplerViewTable::kMaxHandleSlot); + return nullptr; + } + const Uint32 liveGen = g_backendSamplerViews.LiveGenAt(view.Slot); + if (liveGen != 0 && liveGen > view.Gen) { + MGLOG_E_ONCE("MGPipe: sampler-view handle {%u, %u} names a generation BEHIND the " + "live twin's %u - refusing rather than dropping the incumbent", + view.Slot, view.Gen, liveGen); + return nullptr; + } + auto& twin = g_backendSamplerViews.GetOrCreate(view); + if (!twin) twin = MakeShared(); + return twin.get(); + } + + BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view) { + auto* twin = g_backendSamplerViews.FindByHandle(view); + return twin ? twin->get() : nullptr; + } + + MG_Pipe::MGPipeHandle HandleOfSamplerViewForTexture( + const MG_State::GLState::ITextureObject* textureObject) { + // Through the table's own single-entry front memo rather than straight into + // MGPipeSlots().FindByLifetimeId, which is a hash lookup. HandleOf answers + // identically - the same allocator probe on a miss - and remembers the answer; a + // null answer is deliberately never memoised (SlotTables.h, at HandleOf). + return g_backendSamplerViews.HandleOf(textureObject); + } + } // namespace SamplerViewImpl +#endif + namespace VertexArrayImpl { namespace { SizeT GetDataTypeSize(DataType type) { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 034f3612..632909c6 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -428,6 +428,51 @@ namespace MobileGL::MG_Backend::DirectGLES { return nullptr; } + // P4a (D-B1): resolve-or-create BY THE HANDLE THE CALL CARRIED. This is the shape P3a + // already runs for the buffer family through BackendBufferResourceTable, lifted onto + // the five registries that still mint their own handles off a frontend lifetime id - + // the debt SlotTables.h records against itself at the top of that file. + // + // POINTER, not the reference GetOrCreate(StatePtr) returns, and that is deliberate: + // this call has THREE ways to decline and every one of them has to be visible to the + // caller rather than answered with a parked twin. + // * the legacy arm is running, so there is no slot table to index; + // * the slot is past the table's sanity bound (a corrupt 32-bit slot must not decide + // a vector resize); + // * the generation is BEHIND the live entry's. SlotTables.h:301-321 is the whole + // argument: forward is a recycle and resets the twin, BACKWARD is refused, because + // adopting it would destroy the incumbent LIVE twin's driver ids and then stamp the + // slot back to the dead object's generation - the shape commit d7655247 fixed. + // The refusal is SILENT here and gets its release-build voice at the per-kind resolver + // in Managers.cpp, exactly as GetOrCreateBufferResourceForHandle gives P3a's. + BackendPtr* GetOrCreateByHandle(MG_Pipe::MGPipeHandle handle) { + if (!EsprytSlotTablesEnabled()) return nullptr; + if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; + if (handle.Slot >= SlotTable::kMaxHandleSlot) return nullptr; + const Uint32 liveGen = m_slotTable.LiveGenAt(handle.Slot); + if (liveGen != 0 && liveGen > handle.Gen) return nullptr; + return &m_slotTable.GetOrCreate(handle); + } + + // The generation of the LIVE entry at this slot, or 0. It exists so a caller can + // DIAGNOSE, in a release build where MOBILEGL_ASSERT is inert, the refusal above + // performs silently. + Uint32 LiveGenAt(Uint32 slot) const { + if (!EsprytSlotTablesEnabled()) return 0; + return m_slotTable.LiveGenAt(slot); + } + + // The death half of GetOrCreateByHandle, for a kind whose announcement is its own + // destroy CALL rather than the shared death notice. Hands the twin OUT rather than + // destroying it in place, so the caller reaches whatever the driver id owes - a + // delete, a pool enrolment, a deferred release - with the entry already retired and a + // re-entrant GetOrCreate from the twin's destructor cannot resurrect it. The SLOT is + // not freed: for a handle-keyed kind the CLIENT frees it after the destroy returns. + BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) { + if (!EsprytSlotTablesEnabled()) return BackendPtr{}; + return m_slotTable.ReleaseByHandle(handle); + } + // P2 step e2. STATIC, because a death notice is about an object and not about a // registry instance: it is answered by EVERY table of this kind that exists - this // registry's own, and any by-value copy of it a fixture or a context reset is holding @@ -552,6 +597,68 @@ namespace MobileGL::MG_Backend::DirectGLES { using TwinRegistry = StateBackendObjectRegistry; #endif +#if MOBILEGL_PIPE_PUSH + // ---- P4a (D-K3): one arm resolver per family, beside BufferImpl's two ---- + // + // Four bits and therefore four resolvers, for P3a's reason one level out: a framebuffer + // path that regressed, a texture path that regressed, a sampler path that regressed and a + // program path that regressed are four different findings, and clearing one must not + // disarm the other three. + // + // THE RESOLUTION IS LAZY, at the first use, and never at bring-up. Backend context creation + // runs inside eglMakeCurrent and the integration harness pre-flights exactly that sequence + // in a FORKED CHILD; a child that dies on a signal is reported as "no usable GPU" and every + // scenario in the lane is SKIPPED - the lane goes green having run nothing, on the very + // pair of env vars the A/B is driven with, which is what ROADMAP.md:7 forbids. So a stop + // has to land in a test body, i.e. at the first lookup. That is what the inline latches + // below give: a guard-variable load and a perfectly-predicted branch per consult, and the + // arm dispatch folds into the caller (SlotTables.h's EsprytSlotTablesEnabled argument + // verbatim - every one of these is consulted on the per-draw path). + // + // ALL FOUR CAN REACH NoArm and all four STOP there rather than skipping green, because + // every one of the four legacy arms is compiled under MOBILEGL_PIPE_LEGACY_MEMOS: + // framebuffer - the four g_fboSynced* arrays and StampSyncedFBO; + // texture - the twin's m_prevTextureInfo / m_syncedContentVersion cheap-gate trio; + // samplers - UnitSamplerLookupMemo's WeakPtr arm and SamplerPassMemo's raw + // BackendSamplerObject* rows; + // programs - g_programTwinLookupMemo. + // + // THREE OF THEM CARRY A DEPENDENCY (MGPipe.h, D-K2) and it is diagnosed and REFUSED here + // rather than half-run, the bit-8-requires-bit-7 shape ResolveVertexInputSubsystemArm + // already ships: bit 11 requires bit 10, bit 9 requires bit 10, bit 10 requires bit 7. The + // mirror pairs (10 without 11, 10 without 9, 7 without 10) are all FINE and are said so out + // loud, because an unreachable branch that says something different is how the reachable + // one drifts. Bit 12 depends on nothing: a ShaderCso handle names no texture and no buffer. + // + // ResolveFramebufferSubsystemArm additionally carries D-C3's bring-up refusal: the wire + // array is MGPFramebufferState::Color[8] and GetDynamicParameters().MaxColorAttachments is + // the driver's RAW ES cap, which is not clamped to 8 on this path. A driver reporting more + // would silently truncate the record, so the bit is refused with one MGLOG_E naming the cap + // and the legacy arm runs. Widening the payload is a wire change nobody has evidence for; + // truncating silently is the bug class this phase is closing. + Bool ResolveFramebufferSubsystemArm(); + Bool ResolveTextureResourceSubsystemArm(); + Bool ResolveSamplerSubsystemArm(); + Bool ResolveProgramSubsystemArm(); + + inline Bool FramebufferSubsystemEnabled() { + static const Bool enabled = ResolveFramebufferSubsystemArm(); + return enabled; + } + inline Bool TextureResourceSubsystemEnabled() { + static const Bool enabled = ResolveTextureResourceSubsystemArm(); + return enabled; + } + inline Bool SamplerSubsystemEnabled() { + static const Bool enabled = ResolveSamplerSubsystemArm(); + return enabled; + } + inline Bool ProgramSubsystemEnabled() { + static const Bool enabled = ResolveProgramSubsystemArm(); + return enabled; + } +#endif + namespace BufferImpl { const GLenum TempBufferTarget = GL_ARRAY_BUFFER; @@ -2231,6 +2338,73 @@ namespace MobileGL::MG_Backend::DirectGLES { g_backendSamplerObjects; } // namespace SamplerImpl +#if MOBILEGL_PIPE_PUSH + namespace SamplerViewImpl { + // P4a (D-F2/D-F3): the SIXTH Espryt twin table, and the only one of the six whose kind + // has no frontend object at all. MobileGL has no sampler-view class: GL binds a texture + // to a unit and the sampler uniform's type, the mipmap-completeness predicates and + // IsUndefinedDefaultTexture decide what the shader sees. Gallium's one-view-per-slot IS + // that resolved form, the resolution moves to the CLIENT (ARCHITECTURE.md:206), and + // create_sampler_view carries the restrictions the resolution had to read. + // + // So this twin owns NO DRIVER ID. There is nothing in ES to create for a view; the id + // the unit binds is the texture's, and it lives on BackendTextureObject. What this twin + // is, is the server's MEMO of one resolved view: the record it was built from, keyed on + // that record's serial, plus the two BACKEND-SPECIFIC POST-PROCESSINGS + // ARCHITECTURE.md:206 keeps on the server and which act on the already-resolved set. + // Espryt's is the raw-depth-fetch sampler substitution; Magma's feedback-loop detection + // is its own and is not here. + // + // A twin with no driver id still earns a table: it is what turns "re-derive the + // substitution decision for every sampled unit of every draw" into one serial compare, + // and it is the slot space the client's per-texture SamplerViewCso handle indexes. + // There is deliberately no destructor: nothing here owns a GPU object, so the teardown + // sentinel's whole reason (a twin destructor must not call into an unloaded driver) + // does not apply and the default one is correct in every teardown order. + struct BackendSamplerViewObject { + // The view record as last synced, verbatim. Reading it here rather than re-asking + // the applier is what lets a caller hold the twin across another applier call. + MG_Pipe::MGPSamplerView View{}; + // The applier record's Serial this memo was built from. 0 = never synced, and 0 is + // never a real serial (the applier's counters start at 1), so a zeroed memo is a + // guaranteed miss. + Uint64 SyncedSerial = 0; + // Espryt's post-processing, decided from the RESOLVED set: the view's + // InternalFormat answers IsDepthFormatInternalFormat and the sampler CSO record's + // SamplerParameters answer compareMode / minFilter / mipmapMode / magFilter. The + // decision is re-derived when either serial moves; the sampler serial is kept + // beside it so a sampler mutation alone re-derives it. + Uint64 SyncedSamplerSerial = 0; + Bool NeedsRawDepthFetchSampler = false; + }; + + // Handle-keyed ONLY, exactly like P3a's BackendBufferResourceTable: the StateObject + // parameter names ITextureObject because the template names one and because the view is + // minted off the TEXTURE's lifetime id (D-F2: one view per ITextureObject), which is + // what makes HandleOf below resolve at all. Not one member that would dereference it is + // instantiated - no Find(StateObject*), no ForEachLive - and the handle overloads never + // look at it. + using BackendSamplerViewTable = BackendSlotTable; + extern BackendSamplerViewTable g_backendSamplerViews; + + // Resolve-or-create / resolve-only by the handle the call carried. Neither touches + // MGPipeSlots(): the handle ARRIVED, already minted by the side that owns minting. + BackendSamplerViewObject* GetOrCreateSamplerViewForHandle(MG_Pipe::MGPipeHandle view); + BackendSamplerViewObject* FindSamplerViewForHandle(MG_Pipe::MGPipeHandle view); + + // MONOLITH GLUE, and named as such, the HandleOfBuffer shape: the SamplerViewCso handle + // of a texture this backend is looking at through a frontend object. Legal only because + // the view is minted off the texture's own lifetime id; under a real split neither the + // object nor its lifetime id exists on this side and the handle has to arrive in the + // payload (which, for every path P4a switches over, it does - this is for the paths + // P3b/P4b still owns). + MG_Pipe::MGPipeHandle HandleOfSamplerViewForTexture( + const MG_State::GLState::ITextureObject* textureObject); + } // namespace SamplerViewImpl +#endif + namespace RenderbufferImpl { class BackendRenderbufferObject { public: