From fa77b1874f69da41c66506cc108c8c46eda924f2 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 6 Oct 2026 20:55:16 -0400 Subject: [PATCH] [Refactor] (P13): Espryt's frontend-keyed slot-table entries and the apply-thread refusals go, death notices release twins by handle, and the link ratchet refuses any '# P13' line (49 -> 36 symbols). --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 689 +++-------------- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 728 +----------------- MobileGL/MG_Backend/DirectGLES/Managers.h | 299 +------ MobileGL/MG_Backend/DirectGLES/SlotTables.h | 359 +-------- .../BackendObject_DirectVulkan.cpp | 9 +- .../MG_Backend/DirectVulkan/DirectVulkan.cpp | 3 +- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 5 +- MobileGL/MG_Impl/Pipe/PipeFill.cpp | 15 +- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp | 25 - MobileGL/MG_Impl/Pipe/SlotAllocator.h | 51 +- .../Harness/P4aSeamPeek.cpp | 7 +- .../Harness/PipeApplyPeek.cpp | 20 +- MobileGL/MG_Pipe/PipeApply.cpp | 12 + MobileGL/MG_Pipe/PipeApply.h | 10 + MobileGL/MG_Remote/Client/WireTables.cpp | 3 +- .../MG_State/GLState/StateObjectDeathNotice.h | 12 +- MobileGL/MG_Test/SanityTest.cpp | 495 +++++------- MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 152 +--- MobileGL/MG_Test/Wire/ServerLoopTest.cpp | 43 -- scripts/data/link_ratchet_baseline.txt | 19 +- scripts/link_ratchet.py | 34 +- 21 files changed, 492 insertions(+), 2498 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 2a0eceb8..b1b3ce98 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -568,52 +568,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } - void SyncBufferBindingPoints(BufferTarget target, GLenum glTarget) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // Only sync up to the high-water mark of app-touched points; the fixed array is 84 - // deep but apps bind a handful, so the never-touched tail is already at GL default 0. - auto bindingPointCnt = MG_Pipe::gPipeInputs.GetTouchedBufferBindingPointCount(target); - // ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5 - // minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend - // index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost - // by stopping: this frontend-indexed pass exists for the compute path, and the - // per-program rebind in BindCurrentProgramWithResources - which is what actually feeds - // a shader - remaps every block a program declares onto a compacted ES point, so a - // block bound at GL point 83 still reaches its shader. - if (target == BufferTarget::Uniform && g_GLESCapabilities.MaxUniformBufferBindings > 0) { - bindingPointCnt = std::min(bindingPointCnt, - static_cast(g_GLESCapabilities.MaxUniformBufferBindings)); - } - for (SizeT i = 0; i < bindingPointCnt; ++i) { - auto& point = MG_Pipe::gPipeInputs.GetBufferBindingPoint(target, i); - auto& obj = point.GetBoundObject(); - if (!obj) { - BindBufferBaseCached(glTarget, static_cast(i), 0); - continue; - } - - auto* backendResource = EnsureBufferResource(obj); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer found for %s binding point %zu.", - MG_Util::ConvertGLEnumToString(glTarget).c_str(), i); - continue; - } - - const auto& range = point.GetRange(); - auto backendBufferId = backendResource->id; - if (range.start == 0 && range.end >= obj->GetSize()) { - BindBufferBaseCached(glTarget, static_cast(i), backendBufferId); - } else { - const auto start = std::min(range.start, obj->GetSize()); - const auto end = std::min(range.end, obj->GetSize()); - BindBufferRangeCached(glTarget, static_cast(i), backendBufferId, - static_cast(start), static_cast(end - start)); - } - } - } - void SyncAtomicCounterBuffers(const Vector& glBindings, Int esslBindingTop) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -623,207 +577,63 @@ namespace MobileGL::MG_Backend::DirectGLES { // program declares - and only the points themselves move sides. The bound is the // applier's window instead of GetBufferBindingPointCount's sticky forward, which is // the row this site was the Espryt reader of. - if (BindingPointsComeFromRecords()) { - const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); - const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; - const SizeT windowEnd = static_cast(windowStart) + - st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; - for (const Int glBinding : glBindings) { - if (glBinding < 0) continue; - const Int esslBinding = esslBindingTop - glBinding; - // Already diagnosed once when the block was transpiled; nothing was bound to - // it there either, so there is nothing to unbind here. - if (esslBinding < 0) continue; - // OUTSIDE THE WINDOW IS "NOTHING BOUND", which is what the frontend array's - // default says too - so it takes the unbind arm rather than being skipped. - // Skipping would leave whatever the driver last had at that ESSL point, - // which is the hole the frontend walk's `continue` never had because its - // bound was the whole 84-point array. - const MG_Pipe::MGPBufferRange* entry = nullptr; - if (static_cast(glBinding) >= windowStart && - static_cast(glBinding) < windowEnd) { - entry = &st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter] - [static_cast(glBinding)]; - } - if (entry == nullptr || MG_Pipe::MGPipeHandleIsNull(entry->Res)) { - BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), 0); - continue; - } - auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry->Res); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d " - "(handle {%u, %u}).", - glBinding, entry->Res.Slot, entry->Res.Gen); - continue; - } - if (entry->Offset == 0 && entry->Size == MG_Pipe::kMGPipeWholeBuffer) { - BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), - backendResource->id); - } else { - const SizeT storage = backendResource->storageSize; - const SizeT rangeStart = std::min(static_cast(entry->Offset), storage); - const SizeT rangeEnd = - std::min(static_cast(entry->Offset + entry->Size), storage); - BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), - backendResource->id, static_cast(rangeStart), - static_cast(rangeEnd - rangeStart)); - } - // AND NO MarkBufferGpuWritten HERE. It was the second of the three backend - // GPU-write sites and it reached into a client object; the client's - // MarkAtomicCounterBindings (GpuWritePending.cpp) already marks the same set - // from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched - // counter point rather than only the ones this program declares, which is - // the safe direction for an over-approximate set. - // - // P8-C: the server's own twin is marked instead, for the server's own CPU - // readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a - // parameter or element buffer is read on this side. - backendResource->serverGpuWritten = true; - } - return; - } - const SizeT pointCount = MG_Pipe::gPipeInputs.GetBufferBindingPointCount(BufferTarget::AtomicCounter); + const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); + const Uint32 windowStart = st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; + const SizeT windowEnd = static_cast(windowStart) + + st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter]; for (const Int glBinding : glBindings) { - if (glBinding < 0 || static_cast(glBinding) >= pointCount) continue; + if (glBinding < 0) continue; const Int esslBinding = esslBindingTop - glBinding; - // Already diagnosed once when the block was transpiled; nothing was bound to it - // there either, so there is nothing to unbind here. + // Already diagnosed once when the block was transpiled; nothing was bound to + // it there either, so there is nothing to unbind here. if (esslBinding < 0) continue; - auto& point = MG_Pipe::gPipeInputs.GetBufferBindingPoint(BufferTarget::AtomicCounter, - static_cast(glBinding)); - auto& obj = point.GetBoundObject(); - if (!obj) { + // OUTSIDE THE WINDOW IS "NOTHING BOUND", which is what the frontend array's + // default says too - so it takes the unbind arm rather than being skipped. + // Skipping would leave whatever the driver last had at that ESSL point, + // which is the hole the frontend walk's `continue` never had because its + // bound was the whole 84-point array. + const MG_Pipe::MGPBufferRange* entry = nullptr; + if (static_cast(glBinding) >= windowStart && + static_cast(glBinding) < windowEnd) { + entry = &st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassAtomicCounter] + [static_cast(glBinding)]; + } + if (entry == nullptr || MG_Pipe::MGPipeHandleIsNull(entry->Res)) { BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), 0); continue; } - - auto* backendResource = EnsureBufferResource(obj); + auto* backendResource = EnsureBufferResourceForHandle(nullptr, entry->Res); if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d.", glBinding); + MGLOG_E_ONCE("No backend buffer found for atomic counter binding point %d " + "(handle {%u, %u}).", + glBinding, entry->Res.Slot, entry->Res.Gen); continue; } - - const auto& range = point.GetRange(); - if (range.start == 0 && range.end >= obj->GetSize()) { + if (entry->Offset == 0 && entry->Size == MG_Pipe::kMGPipeWholeBuffer) { BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), backendResource->id); } else { - const auto start = std::min(range.start, obj->GetSize()); - const auto end = std::min(range.end, obj->GetSize()); + const SizeT storage = backendResource->storageSize; + const SizeT rangeStart = std::min(static_cast(entry->Offset), storage); + const SizeT rangeEnd = + std::min(static_cast(entry->Offset + entry->Size), storage); BindBufferRangeCached(GL_SHADER_STORAGE_BUFFER, static_cast(esslBinding), - backendResource->id, static_cast(start), - static_cast(end - start)); + backendResource->id, static_cast(rangeStart), + static_cast(rangeEnd - rangeStart)); } - // The whole point of a counter is that the shader INCREMENTS it, and every - // conformance case reads the result back with glMapBufferRange or - // glGetBufferSubData - which serve the frontend's CPU shadow until the buffer is - // flagged (BufferObject::SyncGpuWrites), exactly as for a storage buffer. - MarkBufferGpuWritten(obj); - } - } - - // The handle arm of the resolved-draw-buffers memo (D-G4). Two substitutions and - // nothing else: - // - // validity the frontend VAO's wrapping configuration version is replaced by the - // bound vertex-elements CSO ({slot, gen} AND its server-owned content - // serial) plus the vertex-buffer set's own serial - so an emission the - // suppressor let through is what re-opens the memo, not a counter the - // backend reads out of the frontend; - // identity an entry names its resource by handle and the clean probe compares that, - // instead of a raw frontend address that a successor object can reproduce. - // - // The WALK itself still reads the frontend VAO's attributes, and deliberately: the - // buffers a draw needs ensured is a pull site P3a does not migrate (dirty bits 15-17 - // are P4b's and the resolution move is P8's), and EnsureBufferResource still owes - // BufferObject::SyncPersistentMappedRange one call (D-N). - void SyncVaoAttributeBuffersByHandle(const SharedPtr& currentVAOObject, - VertexArrayImpl::BackendVertexArrayObject::ResolvedDrawBuffers* memo, - Uint64 bufferEpoch) { - const auto& st = MG_Pipe::MGPipeApplier(); - const MG_Pipe::MGPipeHandle elements = st.BoundVertexElements; - Uint64 elementsSerial = 0; - Bool haveElementsRecord = false; - if (!MG_Pipe::MGPipeHandleIsNull(elements) && elements.Slot < st.VertexElementsCsos.size()) { - const auto& record = st.VertexElementsCsos[elements.Slot]; - if (record.Live && record.Gen == elements.Gen) { - elementsSerial = record.ContentSerial; - haveElementsRecord = true; - } - } - const Uint64 buffersSerial = st.VertexBuffersSerial; - - // NO LIVE ELEMENTS RECORD IS A MISS, NEVER A HIT. With none, the key above is - // {null, 0, buffersSerial} - a key that describes no configuration at all and that - // NEVER CHANGES while the state stays that way, so a memo stamped with it would hit - // on every later draw of a VAO whose attributes have moved. That state is reachable: - // MGPipeApplierReset() empties VertexElementsCsos and BoundVertexElements at every - // change of the current context, and the client re-emits only at its next - // create/bind. The legacy arm's configuration version caught exactly this by moving. - const Bool memoKeyIsMeaningful = haveElementsRecord; - - if (memo && memo->valid && memoKeyIsMeaningful && memo->elementsHandle == elements && - memo->elementsSerial == elementsSerial && memo->buffersSerial == buffersSerial) { - if (memo->vboCleanEpoch != bufferEpoch) { - Bool allClean = true; - for (Uint i = 0; i < memo->count; ++i) { - auto& entry = memo->entries[i]; - // entry.frontend is the same object the legacy arm probes and is kept - // alive by the VAO attribute's SharedPtr for as long as this memo is - // valid; it answers the live-map question no record carries in P3a. - if (IsBufferDrawCleanByHandle(entry.handle, entry.resource, entry.frontend)) continue; - allClean = false; - entry.resource = - EnsureBufferResource(currentVAOObject->GetAttribute(entry.attribIndex).Buffer); - } - memo->vboCleanEpoch = allClean ? bufferEpoch : 0; - } - return; - } - - // Full walk, once per distinct buffer, rebuilding the memo as it goes. - MG_State::GLState::BufferObject* syncedBuffers[MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS]; - Uint syncedBufferCount = 0; - const auto& allAttributes = currentVAOObject->GetAllAttributes(); - for (Uint attribIndex = 0; attribIndex < allAttributes.size(); ++attribIndex) { - const auto& attrib = allAttributes[attribIndex]; - if (!attrib.Enabled) continue; - const auto& bufferObject = attrib.Buffer; - if (!bufferObject) continue; - - auto* const bufferKey = bufferObject.get(); - Bool alreadySynced = false; - for (Uint i = 0; i < syncedBufferCount; ++i) { - if (syncedBuffers[i] == bufferKey) { - alreadySynced = true; - break; - } - } - if (alreadySynced) continue; - - auto* resource = EnsureBufferResource(bufferObject); - if (memo) { - auto& entry = memo->entries[syncedBufferCount]; - entry.frontend = bufferKey; - entry.attribIndex = static_cast(attribIndex); - entry.resource = resource; - entry.handle = HandleOfBuffer(bufferKey); - } - syncedBuffers[syncedBufferCount++] = bufferKey; - } - if (memo) { - memo->count = syncedBufferCount; - memo->elementsHandle = elements; - memo->elementsSerial = elementsSerial; - memo->buffersSerial = buffersSerial; - // Only a key that describes a real configuration is worth remembering; see - // memoKeyIsMeaningful above. The walk still ran and the buffers are ensured - - // this only refuses to let the NEXT draw skip it. - memo->valid = memoKeyIsMeaningful; - // Rebuilt via EnsureBufferResource, not probed clean: the next probe pass - // stamps the epoch. - memo->vboCleanEpoch = 0; + // AND NO MarkBufferGpuWritten HERE. It was the second of the three backend + // GPU-write sites and it reached into a client object; the client's + // MarkAtomicCounterBindings (GpuWritePending.cpp) already marks the same set + // from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched + // counter point rather than only the ones this program declares, which is + // the safe direction for an over-approximate set. + // + // P8-C: the server's own twin is marked instead, for the server's own CPU + // readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a + // parameter or element buffer is read on this side. + backendResource->serverGpuWritten = true; } + return; } // P5e (vi), CONTRACT-P5E §5.1: THE SAME MEMO, THE SAME THREE-VALUE KEY, AND A WALK THAT @@ -1033,13 +843,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // binder (e.g. Flywheel's indirect vertex shaders pull instance data from storage buffers). // P5e (sb, §5.6): the record arm, selected by transport + bit 13. Same `if (…) else` // shape the indirect buffer above already uses. - if (BindingPointsComeFromRecords()) { - SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, - GL_SHADER_STORAGE_BUFFER); - } else - { - SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER); - } + SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, + GL_SHADER_STORAGE_BUFFER); } void SyncComputeBuffers(Bool includeDispatchIndirectBuffer) { @@ -1054,16 +859,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // is why the uniform class is emitted at all: compute does NOT go through the // per-program block remap in BindCurrentProgramWithResources, so these are the // points a compute shader actually reads. - if (BindingPointsComeFromRecords()) { - SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassUniform, - GL_UNIFORM_BUFFER); - SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, - GL_SHADER_STORAGE_BUFFER); - } else - { - SyncBufferBindingPoints(BufferTarget::Uniform, GL_UNIFORM_BUFFER); - SyncBufferBindingPoints(BufferTarget::ShaderStorage, GL_SHADER_STORAGE_BUFFER); - } + SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassUniform, + GL_UNIFORM_BUFFER); + SyncBufferBindingPointsByRecord(MG_Pipe::kMGPipeShaderBufferClassShaderStorage, + GL_SHADER_STORAGE_BUFFER); if (includeDispatchIndirectBuffer) { // P5c (hd, CONTRACT-P5C §3.5): see the draw-indirect twin above - the verb // record's handle, never the frontend binding slot or the client allocator. @@ -1805,41 +1604,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } // namespace XfbImpl namespace VertexArrayImpl { - // Resolve-or-create the VAO's backend twin, once per draw: PrepareForDraw passes - // the result to the buffer sync (resolved-buffers memo host), the VAO sync and - // the draw-time bind, which each used to run their own registry Find. The raw - // pointer stays valid for the whole draw: the frontend VAO is pinned by the - // context binding, and a live object's twin is never erased nor replaced - on the - // legacy arm that is TwinLookupMemo's contract, and on the {slot, gen} arm it is - // simply that nothing but the sweep frees a slot and the sweep only takes slots - // whose frontend object is already gone. - // MONOLITH GLUE AS OF P5e (vi), CONTRACT-P5E §4.1 / §5.8, AND THE SCOPE IS GONE WITH THE - // DEBT IT NAMED. This overload is reached only when there IS a frontend VAO to resolve - // from, i.e. from the push-monolith and legacy arms of PrepareForDraw; a live transport - // takes ResolveVaoTwin(st.BoundVertexElements) instead (Managers.cpp, id's body). The - // MGPipeFrontendKeyedRegistryScope that used to wrap this - one of the three scope sites - // this family owned - is DELETED rather than narrowed, and that deletion is what makes - // the revert loud: putting `Find(vao.get())` back on the transport arm now aborts - // Fatal{RoleViolation, "MGPipeSlots"} from HandleOf instead of being quietly exempted. - BackendVertexArrayObject* ResolveVaoTwin(const SharedPtr& vao) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (EsprytSlotTablesEnabled()) { - // No memo on this arm. The memo existed to turn the registry's hash probe into - // an array index; the slot table's Find already IS the array index, and the - // memo's whole safety argument - owner-equality against a recycled heap address - // - is answered by {slot, gen} instead of re-derived per lookup. - auto* slot = g_backendVertexArrayObjects.Find(vao.get()); - auto& backendObj = slot ? *slot : g_backendVertexArrayObjects.GetOrCreate(vao); - if (!backendObj) { - backendObj = MakeShared(); - } - return backendObj.get(); - } - return nullptr; - } - // P5e (vi): the record arm's entry, and it takes NO frontend object - CONTRACT-P5E §4.1 // ("the object parameter is deleted from the transport overload, not defaulted to // null"). A null twin here is the null BoundVertexElements, i.e. "no VAO bound", which @@ -1850,9 +1614,6 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - // Kept for symmetry with the overload above and free on this arm: the slot table's - // sweep is a no-op (Managers.h's CollectGarbageIfNeeded on the handle registry). - g_backendVertexArrayObjects.CollectGarbageIfNeeded(); if (!vaoTwin) return; vaoTwin->SyncToBackendFromApplier(); } @@ -1943,12 +1704,13 @@ namespace MobileGL::MG_Backend::DirectGLES { static void SyncClientSideVertexArraysForDrawArrays(GLint first, GLsizei count) { if (!BufferImpl::AnyClientSideVertexArrayInRecord()) return; - if (MG_Pipe::MGPipeApplierIsUnbarrieredApply()) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - a draw carrying a " - "client-memory vertex array is being applied UNBARRIERED. Both the " - "client's refusal (Fatal{UnmigratedVerb, \"DrawArrays+CLIENT_ARRAYS\"}) " - "and escalation (ii) of CONTRACT-P5E §2.1 exist to make this " - "unreachable; the bytes below are the application's and are moving"); + // P13: client-memory arrays are monolith-only - under a transport the client refuses + // them (Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}) before any record goes out, + // so a record naming one here means the bytes below belong to another process. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + MGLOG_F("MGPipe: Fatal{RoleViolation, \"ClientArrays\"} - a draw carrying a " + "client-memory vertex array reached the backend under a transport; the " + "client's refusal exists to make this unreachable"); std::abort(); } auto* twin = ResolveVaoTwin(MG_Pipe::MGPipeApplier().BoundVertexElements); @@ -2220,87 +1982,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } // namespace VertexArrayImpl namespace TextureImpl { - SharedPtr& SyncTextureObjectToBackend( - const SharedPtr& textureObject, - Bool imageBindableStorageRequired) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - // P5c (G6, CONTRACT-P5C §5.4): this sync still arrives holding the FRONTEND object - // (the object-class rows) and resolves its twin by frontend identity - the - // frontend-keyed registry P3b/P4b rekeys onto handles. The probe (and the mint on - // a first sync) is named debt inside the scope, not an unwrapped violation. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - auto* backendTextureSlot = g_backendTextureObjects.Find(textureObject.get()); - auto& backendSlot = backendTextureSlot ? *backendTextureSlot - : g_backendTextureObjects.GetOrCreate(textureObject); - if (!backendSlot) { - backendSlot = MakeShared(); - } - - // A by-VALUE copy of the twin for the duration of the syncs below. `backendSlot` is a - // reference INTO the open-addressed registry, and syncing can RE-ENTER this function: - // a texture created by glTextureView has to sync the texture whose storage it views - // first (SyncTextureViewToBackend), and that nested call may insert, grow the map and - // relocate every entry - leaving the reference dangling. Holding the object itself - // keeps the calls below working on the right twin regardless; the slot is re-resolved - // at the end for the reference this function returns. - // - // The slot-table arm has no such hazard - an entry is an array element and a nested - // insert can only reallocate the vector, which the re-resolve at the tail already - // handles - so the copy is a refcount it does not need to pay. It keeps the copy for - // exactly one thing: holding the twin alive across the nested sync. - const SharedPtr backendObj = backendSlot; - - if (imageBindableStorageRequired) { - backendObj->RequireImageBindableStorage(textureObject); - } - backendObj->SyncTextureParamsToBackend(textureObject); - backendObj->SyncBuiltinSamplerToBackend(textureObject); - backendObj->SyncMipmapsToBackend(textureObject); - // The storage sync may RE-MINT the driver texture - a fresh glTexStorage after a - // shape change, an image-bindable widening, or the glTextureView that an - // ARB_texture_view view is created on - which discards every parameter the two calls - // above just pushed. Re-push them here rather than leaving it to the next sync: the - // very next thing that happens is usually the draw this sync was run for, and until - // the filters land the new texture is at the ES defaults, which for a single-level or - // integer texture is not merely mis-filtered but INCOMPLETE, i.e. it samples zero. - if (backendObj->NeedsParameterResync()) { - backendObj->SyncTextureParamsToBackend(textureObject); - backendObj->SyncBuiltinSamplerToBackend(textureObject); - } - - if (EsprytSlotTablesEnabled()) { - // One re-resolve, and only because a nested GetOrCreate may have GROWN the - // vector and moved the element; the entry itself cannot have been erased, since - // nothing on this arm erases a live slot. No second Find-or-create, no - // put-the-twin-back repair. - auto* slot = g_backendTextureObjects.Find(textureObject.get()); - MOBILEGL_ASSERT(slot != nullptr && *slot != nullptr, - "the texture twin resolved at entry is gone after its own sync"); - if (slot != nullptr && *slot != nullptr) { - return *slot; - } - // Cannot happen - the caller holds the frontend object, so its slot cannot be - // reclaimed underneath this call - but the return is a reference, and a null - // deref in a release build is a worse way to learn that than a re-created twin. - auto& repaired = g_backendTextureObjects.GetOrCreate(textureObject); - if (!repaired) { - repaired = backendObj; - } - return repaired; - } - auto* refreshedSlot = g_backendTextureObjects.Find(textureObject.get()); - auto& refreshedBackendObj = refreshedSlot ? *refreshedSlot - : g_backendTextureObjects.GetOrCreate(textureObject); - if (!refreshedBackendObj) { - // A collection ran during the nested sync and took this slot with it; put the - // twin the caller is about to use back, rather than handing back an empty one. - refreshedBackendObj = backendObj; - } - return refreshedBackendObj; - } - // Identity snapshot of what one texture unit has bound: the object in every binding // slot plus the unit's sampler object. This - not the context's texture bind // generation - is what the per-draw texture memos key on: the generation also bumps @@ -2846,7 +2527,6 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - g_backendTextureObjects.CollectGarbageIfNeeded(); // All textures we need are: // 1. textures bound to texture units (TODO: only sync ones that are used in current program) @@ -3710,9 +3390,6 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - g_backendFramebufferObjects.CollectGarbageIfNeeded(); - TextureImpl::g_backendTextureObjects.CollectGarbageIfNeeded(); - RenderbufferImpl::g_backendRenderbufferObjects.CollectGarbageIfNeeded(); // The handle arm first, and it declines rather than half-running: with no record // yet it hands the walk straight back to the pre-handle arm below, unchanged. @@ -4826,8 +4503,6 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - g_backendProgramObjects.CollectGarbageIfNeeded(); - SamplerImpl::g_backendSamplerObjects.CollectGarbageIfNeeded(); g_currentDrawFrontendProgram = nullptr; g_currentDrawBackendProgram = nullptr; @@ -5245,48 +4920,6 @@ namespace MobileGL::MG_Backend::DirectGLES { static Array g_unitSamplerLookupMemos; - static SamplerImpl::BackendSamplerObject* ResolveUnitSamplerBackend( - Int unit, const SharedPtr& samplerObject) { - // P5e (tx2), CONTRACT-P5E §4.2's last paragraph: THE IDENTITY SAMPLER FAMILY IS DELETED - // UNDER A TRANSPORT AND BECOMES A NAMED REFUSAL. - // - // This function is the apply-thread MINT for a unit the record set does not cover: it - // probes the client allocator with HandleOf(samplerObject), keeps a WeakPtr to a frontend - // sampler in a per-unit memo, and its caller in the program pass creates a twin off the - // frontend object when the lookup misses. Every one of those is a rule-F violation, and - // the window rule (§5.3) makes the case it exists for UNREPRESENTABLE: a unit whose - // BoundSamplerStates[u] is null while the frontend held a sampler is a MISSING RECORD, - // not a unit to be served from the client's object. - MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - unit %d holds a " - "frontend sampler object that the applied bind_sampler_states window does not " - "describe. Minting a twin for it would probe the client's allocator from the " - "apply thread; CONTRACT-P5E.md §4.2 deletes the identity sampler family under a " - "transport and §5.3 makes the missing entry unrepresentable", - static_cast(unit)); - std::abort(); - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside the - // scope - the refusal above is what retires it under a transport. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - auto& memo = g_unitSamplerLookupMemos[static_cast(unit)]; - if (EsprytSlotTablesEnabled()) { - const MG_Pipe::MGPipeHandle handle = - SamplerImpl::g_backendSamplerObjects.HandleOf(samplerObject.get()); - if (memo.backend && !MG_Pipe::MGPipeHandleIsNull(handle) && memo.frontendHandle == handle) { - return memo.backend; - } - auto* slot = SamplerImpl::g_backendSamplerObjects.FindByHandle(handle); - if (slot && *slot) { - // A MISS is still never cached: the twin may not exist yet when the unit pass - // runs, because the program pass creates it later in the same draw. - memo.frontendHandle = handle; - memo.backend = slot->get(); - return memo.backend; - } - return nullptr; - } - return nullptr; - } - // Puts each touched unit's frontend sampler object on the backend unit. Not part // of the texture memo below (BindCurrentProgramWithResources may rewrite sampled // units' samplers right after this runs - raw-depth-fetch substitution, @@ -5419,21 +5052,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } walkedFromRecords = true; } - if (!walkedFromRecords) - { - for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { - const auto& samplerObject = MG_Pipe::gPipeInputs.GetTextureUnitObject(unit).GetSamplerObject(); - if (samplerObject) { - if (auto* backendSampler = ResolveUnitSamplerBackend(unit, samplerObject)) { - backendSampler->Bind(unit); - } - } else { - // Symmetric with the bind above: a sampler object left on the unit by an - // earlier draw keeps being applied, and on a multisample texture - which - // takes no sampler object at all - the draw is rejected outright. - SamplerImpl::UnbindSampler(unit); - } - } + if (!walkedFromRecords) { + // P13: no bind_sampler_states window applied, i.e. no unit holds a sampler object - + // the record arm emits one for every unit that does. Unbind, so a sampler object + // left on a unit by an earlier draw is not applied to this one. + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) SamplerImpl::UnbindSampler(unit); } g_unitSamplerWalkContextId = keys.contextId; g_unitSamplerWalkEpoch = keys.unitBindingsEpoch; @@ -5863,92 +5486,58 @@ namespace MobileGL::MG_Backend::DirectGLES { // same one - Start is 0 by contract - so `binding` addresses the // applier's window directly, and a binding at or above Count means // "nothing bound", which is what the frontend array's default said too. - if (BufferImpl::BindingPointsComeFromRecords()) { - const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); - const Uint32 windowStart = - st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassUniform]; - const SizeT windowEnd = - static_cast(windowStart) + - st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassUniform]; - if (static_cast(binding) < windowStart || - static_cast(binding) >= windowEnd) { - continue; // nothing bound there - the frontend arm's `if (bufferObj)` - } - const MG_Pipe::MGPBufferRange& entry = - st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassUniform] - [static_cast(binding)]; - if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) continue; - // The clean probe is the SAME five questions IsBufferDrawClean asks - // the frontend object, asked by handle: the twin's identity, the - // resource record's Serial against the twin's synced one, and the - // pending-work sets - all server-owned (D-A4). The drawCleanEpoch - // short-circuit is unchanged; it was never the point read. - auto* backendResource = BufferImpl::FindBufferResourceForHandle(entry.Res); - if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) { - if (backendResource && BufferImpl::IsBufferDrawCleanByHandle( - entry.Res, backendResource, nullptr)) { - backendResource->drawCleanEpoch = bufferEpoch; - } else { - backendResource = - BufferImpl::EnsureBufferResourceForHandle(nullptr, entry.Res); - } - } - if (backendResource && backendResource->id != 0) { - // WHOLE-VS-RANGE IS THE RECORD's TEST, not `range.end == 0`: a - // base binding travels as kMGPipeWholeBuffer precisely so the - // extent is re-resolved HERE, against the storage this server - // holds, rather than frozen at the client's emission. - if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) { - BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding, - backendResource->id); - } else { - const SizeT storage = backendResource->storageSize; - const SizeT rangeStart = - std::min(static_cast(entry.Offset), storage); - const SizeT rangeEnd = std::min( - static_cast(entry.Offset + entry.Size), storage); - BufferImpl::BindBufferRangeCached( - GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id, - static_cast(rangeStart), - static_cast(rangeEnd - rangeStart)); - } - } else { - MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO."); - } - continue; + const MG_Pipe::MGPipeApplierState& st = MG_Pipe::MGPipeApplier(); + const Uint32 windowStart = + st.ShaderBufferStart[MG_Pipe::kMGPipeShaderBufferClassUniform]; + const SizeT windowEnd = + static_cast(windowStart) + + st.ShaderBufferCount[MG_Pipe::kMGPipeShaderBufferClassUniform]; + if (static_cast(binding) < windowStart || + static_cast(binding) >= windowEnd) { + continue; // nothing bound there - the frontend arm's `if (bufferObj)` } - auto& point = MG_Pipe::gPipeInputs.GetBufferBindingPoint(BufferTarget::Uniform, binding); - auto& bufferObj = point.GetBoundObject(); - auto range = point.GetRange(); - - if (bufferObj) { - // Clean-probe (or epoch-stamped) fast path before the full - // EnsureBufferResource: an unchanged static UBO needs no - // storage work, only its binding re-established below. - auto* backendResource = BufferImpl::GetBufferResource(bufferObj.get()); - if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) { - if (backendResource && - BufferImpl::IsBufferDrawClean(bufferObj.get(), backendResource)) { - backendResource->drawCleanEpoch = bufferEpoch; - } else { - backendResource = BufferImpl::EnsureBufferResource(bufferObj); - } - } - if (backendResource && backendResource->id != 0) { - // glBindBufferBase/Range set the generic GL_UNIFORM_BUFFER binding - // as a side effect, so no separate BindBufferId is needed here. - if (range.end == 0) { - BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding, - backendResource->id); - } else { - BufferImpl::BindBufferRangeCached( - GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id, - (GLintptr)range.start, (GLintptr)(range.end - range.start)); - } + const MG_Pipe::MGPBufferRange& entry = + st.BoundShaderBuffers[MG_Pipe::kMGPipeShaderBufferClassUniform] + [static_cast(binding)]; + if (MG_Pipe::MGPipeHandleIsNull(entry.Res)) continue; + // The clean probe is the SAME five questions IsBufferDrawClean asks + // the frontend object, asked by handle: the twin's identity, the + // resource record's Serial against the twin's synced one, and the + // pending-work sets - all server-owned (D-A4). The drawCleanEpoch + // short-circuit is unchanged; it was never the point read. + auto* backendResource = BufferImpl::FindBufferResourceForHandle(entry.Res); + if (!backendResource || backendResource->drawCleanEpoch != bufferEpoch) { + if (backendResource && BufferImpl::IsBufferDrawCleanByHandle( + entry.Res, backendResource, nullptr)) { + backendResource->drawCleanEpoch = bufferEpoch; } else { - MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO."); + backendResource = + BufferImpl::EnsureBufferResourceForHandle(nullptr, entry.Res); } } + if (backendResource && backendResource->id != 0) { + // WHOLE-VS-RANGE IS THE RECORD's TEST, not `range.end == 0`: a + // base binding travels as kMGPipeWholeBuffer precisely so the + // extent is re-resolved HERE, against the storage this server + // holds, rather than frozen at the client's emission. + if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) { + BufferImpl::BindBufferBaseCached(GL_UNIFORM_BUFFER, lastUBOBinding, + backendResource->id); + } else { + const SizeT storage = backendResource->storageSize; + const SizeT rangeStart = + std::min(static_cast(entry.Offset), storage); + const SizeT rangeEnd = std::min( + static_cast(entry.Offset + entry.Size), storage); + BufferImpl::BindBufferRangeCached( + GL_UNIFORM_BUFFER, lastUBOBinding, backendResource->id, + static_cast(rangeStart), + static_cast(rangeEnd - rangeStart)); + } + } else { + MGLOG_E_ONCE("No backend buffer found for UBO binding, cannot bind UBO."); + } + continue; } } @@ -6906,42 +6495,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - // MONOLITH GLUE, AND LOUD ABOUT IT AS OF P5e (vi). CONTRACT-P5E §5.1 makes the restart - // substitution read st.IndexBuffer.Res, and the constructor above already does: its - // transport arm resolves the element buffer and the bytes from the applier and its own - // staged shadow (P5c hd), and it sets serverElementBinding >= 0 BEFORE the null test, - // so the tail at BoundElementArrayBufferId() is unreachable with a live transport. - // These two are therefore only called from the monolith branch - and the refusal below - // is what keeps that a fact rather than a reading: a future caller that reaches them - // from the apply thread aborts by name instead of pulling GetBoundVertexArray behind - // the family's back. - void RefuseElementArrayBufferFromTheFrontend(const char* entry) { - MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - %s read the frontend VAO's " - "element-array slot with a live transport. The index buffer of this family is " - "MGPipeApplier().IndexBuffer.Res (CONTRACT-P5E §5.1); the frontend slot is " - "monolith glue and answers for whatever the CLIENT has bound now, not for the " - "record this draw is being applied from", - entry); - std::abort(); - } - - const SharedPtr& BoundElementArrayBuffer() { - static const SharedPtr none; - RefuseElementArrayBufferFromTheFrontend("BoundElementArrayBuffer"); - const auto& vao = MG_Pipe::gPipeInputs.GetBoundVertexArray(); - if (!vao) return none; - return vao->GetIndexBufferBindingSlot().GetBoundObject(); - } - - // The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what the - // substitution has to put back. - Uint BoundElementArrayBufferId() { - RefuseElementArrayBufferFromTheFrontend("BoundElementArrayBufferId"); - const auto& ibo = BoundElementArrayBuffer(); - if (!ibo) return 0; - const auto* resource = BufferImpl::EnsureBufferResource(ibo); - return resource ? resource->id : 0; - } } // namespace RestartSubstitutionKind ResolveRestartSubstitution(GLenum indexType) { @@ -7124,12 +6677,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return; } m_previousBinding = 0; - if (serverElementBinding >= 0) { - m_previousBinding = static_cast(serverElementBinding); - } else - { - m_previousBinding = BoundElementArrayBufferId(); - } + if (serverElementBinding >= 0) m_previousBinding = static_cast(serverElementBinding); BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, g_restartIndices.id); m_substituted = true; m_indexType = destinationType; @@ -10257,9 +9805,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // dereference. The frontend validator is what keeps this unreachable and what reports // the error the application is owed; declining is only how a future gap up there stops // being a crash. See the level guard in VulkanRenderer::CopyImageSubData. - if (!dstEndpoint.IsRenderbuffer() && dstEndpoint.Texture) { - TextureImpl::NoteDriverSideTextureWrite(dstEndpoint.Texture); - } // The handle arm's endpoint names its texture by handle, and the server has no frontend // object to note. The staged store's half of the note waits for the copy (P8-E, below). const Bool handleArmDestination = @@ -12036,19 +11581,9 @@ namespace MobileGL::MG_Backend::DirectGLES { SplitReadbackPackBuffer(); Bool usePBO = false; GLuint packBufferId = 0; - if (pixelPackBufferObject) { - auto* backendResource = BufferImpl::EnsureBufferResource(pixelPackBufferObject); - MGLOG_D("ReadPixels: Using PBO %u", pixelPackBufferObject->GetExternalIndex()); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("ReadPixels: No backend buffer found for PBO %u.", - pixelPackBufferObject ? pixelPackBufferObject->GetExternalIndex() : 0); - return; - } - usePBO = true; - packBufferId = backendResource->id; - } else { - MGLOG_D("ReadPixels: Not using PBO"); - } + // P13: SplitReadbackPackBuffer() is always null - a pack buffer reaches the readback + // by handle on the verb path - so this entry never packs into a buffer. + MGLOG_D("ReadPixels: Not using PBO"); ScopedPixelPackBuffer packBufferBinding(packBufferId); MGLOG_D("ReadPixels: glReadPixels()"); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index f0616b45..7a73b219 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -211,22 +211,17 @@ namespace MobileGL::MG_Backend::DirectGLES { } namespace { - // P2 step e2's dispatcher: the frontend told us an object died, so free its slot and - // drop its twin NOW. One entry point for all six kinds, because the answer is the same - // for all six - which is why the notice carries the kind rather than there being six - // ops tables. - // - // Each arm below names the registry GLOBAL of its kind, but DestroyByLifetimeId is - // static: it is answered by every table of that kind that exists at the moment - the - // global's own and any by-value copy a fixture or a context reset is holding - and - // the slot goes back once, after all of them have let go (SlotTables.h, the holder - // list). Naming one instance here is a spelling, not a choice of holder. + // P2 step e2's dispatcher: the frontend told us an object died, so drop its twin NOW. + // One entry point for all six kinds, because the answer is the same for all six - which + // is why the notice carries the kind rather than there being six ops tables. The release + // is by the handle the notice carries and reaches every holder of the kind (SlotTables.h, + // the holder list); the client frees the slot afterwards. // // A notice that arrives after exit() has begun is dropped: past that point the twin's // destructor must not call into the driver (see InProcessTeardown()), and the process - // is about to hand every GPU object back anyway. That twin is a deliberate leak, not - // garbage for a later collection - there is none on this arm. - void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { + // is about to hand every GPU object back anyway. That twin is a deliberate leak. + void OnFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, + MG_Pipe::MGPipeHandle handle) { if (InProcessTeardown()) return; #if MOBILEGL_BUILD_DISAGGREGATED // P5c (ct), CONTRACT-P5C.md §5.2: with an active transport the death crosses AS A @@ -241,109 +236,22 @@ namespace MobileGL::MG_Backend::DirectGLES { // nor the client's allocator is touched from there any more. if (MG_Config::Transport != MG_Config::TransportMode::Monolith && !MG_Remote::Server::ServerLoop::OnApplyThread()) { - // P5e (id), CONTRACT-P5E §4.4: THE RECORD IS THE ONLY DELIVERY NOW. What used - // to sit here was a mailbox hop for the ONE shape the record cannot carry - a - // transport configured with no client session at all, i.e. a ServerLoop - // fixture driving a server role with no client, never a real split (there a - // session exists whenever the server does). It posted a stack struct to the - // apply thread, which re-entered this function and ran the switch below; that - // switch's DestroyByLifetimeId probes and FREES in the CLIENT'S allocator from - // the apply thread, which is precisely the read P5e is retiring - and it is - // not a read a barrier could ever make safe, because under run-ahead there is - // no wait behind which the allocator stands still. - // - // WHAT REPLACES IT: nothing, and that is the answer rather than an omission. A - // fixture with no client session has no client allocator worth the name and no - // handle ever crossed, so there is no twin the server built from a record; its - // twins die with the backend at Stop (InProcessTeardown / the registry's own - // destruction), which is what already happened for every death raised past - // `Running() == false`. ID-96 enumerates the fixtures this changed and what - // each one drives instead; every one of them now drives `object_death` by - // handle through the applier, which is the delivery a real split uses. - // ID-96, ANSWERED BY MEASUREMENT: the set of fixtures that lose death delivery - // here is EMPTY. A probe that aborted in exactly this branch (NoSession with a - // RUNNING server loop, i.e. the only shape the deleted hop ever fired for) was - // run over the whole unit lane (2204 entries) and the whole integration-split - // lane (111) and never fired once. The one fixture family that could reach it - - // ServerLoopTest.cpp's ServerFixture / EglServerFixture, the only server role in - // the tree with no client session - has exactly one case that lets a re-keyed - // frontend object die off the apply thread - // (ServerLoopEglTest.FrontendFramebufferDeathDeletesOnTheContextOwner), and that - // case is already refused at its FIRST step by P5c's allocator guard, so its - // `framebuffer.reset()` was unreachable at 2fde7034 too. + // P5e (id), CONTRACT-P5E §4.4: THE RECORD IS THE ONLY DELIVERY. A server role + // with no client session (a ServerLoop fixture) has no twin built from a record, + // so it has nothing to release here; its twins die with the backend at Stop. (void)MG_Remote::Client::EmitObjectDeathRecord(kind, lifetimeId); return; } // Falling through HERE under a transport means the death was raised ON the apply - // thread - the server backend destroying a frontend object it created itself - // (Magma's hidden blit/depth-mipmap resources). Those sites carry their own named - // scope (MagmaP7AllocatorDebtScope), so the allocator probe below is admitted - // there and refused everywhere else; this function no longer opens a scope of its - // own, because the one arm that needed it is the mailbox hop that just went. + // thread - the server backend destroying a frontend object it created itself. #endif - switch (kind) { - case MG_Pipe::MGPipeKind::Texture: - TextureImpl::g_backendTextureObjects.DestroyByLifetimeId(lifetimeId); - break; - case MG_Pipe::MGPipeKind::Framebuffer: - FramebufferImpl::g_backendFramebufferObjects.DestroyByLifetimeId(lifetimeId); - break; - case MG_Pipe::MGPipeKind::Renderbuffer: - RenderbufferImpl::g_backendRenderbufferObjects.DestroyByLifetimeId(lifetimeId); - break; - case MG_Pipe::MGPipeKind::SamplerCso: - SamplerImpl::g_backendSamplerObjects.DestroyByLifetimeId(lifetimeId); - break; - case MG_Pipe::MGPipeKind::ShaderCso: - PrgramImpl::g_backendProgramObjects.DestroyByLifetimeId(lifetimeId); - break; - 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; - 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 - // applier record, raise this notice, free the slot), because the slot is minted - // client-side on every backend and a backend that installs no death ops - which - // Magma did not until P7 wave 2 package C, and whose table even now only EMITS - // the wire record - otherwise leaked the slot and the record per VAO for the - // life of the process. What is left here is the one thing only this - // side can do: drop the driver VAO the twin owns. It is raised while the handle - // still resolves, so OnFrontendObjectDestroyed's shared free (which the other - // five kinds still depend on) is simply the one that gets there first; the - // client's own Free right after it is then a no-op, because Free refuses a slot - // that is no longer live at that generation and the Gen bump rides the next - // handout rather than the free. Double release, no corruption, no abort. - VertexArrayImpl::g_backendVertexArrayObjects.DestroyByLifetimeId(lifetimeId); - break; - default: - // Buffer death crosses as ResourceDestroy (P3a): the catalogue has a call for - // it, so no seventh NotifyStateObjectDestroyed raiser is added. The notice - // exists for kinds that have NO such call - it carries {kind, lifetimeId} and - // not the object, and one entry point serves all of them because the answer is - // the same. The order at the buffer's death is fixed and not negotiable: - // resource_destroy first (the applier clears Live, the backend retires the - // twin), then MGPipeSlots().Free - the allocator erases its lifetimeId -> slot - // mapping on Free, so a notice resolved twice finds nothing the second time. - // On the legacy arm the signal is still BufferBackendOps::OnDestroy. Every - // other kind has no backend twin table here. - break; - } + // P13: the twin is released BY HANDLE, the one the notice carries - the same + // per-kind release the wire's object_death runs on the server + // (ReleaseTwinsForWireObjectDeath below), so no arm resolves a lifetime id in the + // allocator any more. The slot itself goes back in the caller (PipeFill.cpp's + // NotifyAndFree), after this returns. A null handle is an object that never had a + // slot of this kind, hence no twin. + if (!MG_Pipe::MGPipeHandleIsNull(handle)) (void)ReleaseTwinsForWireObjectDeath(handle, kind); } const MG_State::GLState::StateObjectDeathOps g_glesStateObjectDeathOps = { @@ -351,7 +259,6 @@ namespace MobileGL::MG_Backend::DirectGLES { }; } // namespace -#if MOBILEGL_BUILD_DISAGGREGATED // P5e (id), CONTRACT-P5E §4.2 / §4.3: THE TWIN DEATH MOMENT, AND THE ABA ANSWER. // // The moment: the apply of `object_death` / the kind's own delete opcode / `resource_destroy` @@ -410,6 +317,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } } +#if MOBILEGL_BUILD_DISAGGREGATED // P12: see Managers.h. Every table is replaced by an empty one, holders first (views before // the textures they view, framebuffers before their attachments, VAOs before the buffers they // fetch from), so the last reference to each twin goes here and its destructor - answered @@ -3627,67 +3535,10 @@ namespace MobileGL::MG_Backend::DirectGLES { resource.hostBytes = MGL_SERVER_STAGED_ADOPT(resource, ResourceWidthOf(res), bytes, offset, size); } - MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject) { - if (bufferObject == nullptr) return MG_Pipe::kMGPipeNullHandle; - // Through the table's own single-entry front memo rather than straight into - // MGPipeSlots().FindByLifetimeId, which is a hash lookup: this runs inside - // EnsureBufferResource (every SSBO / UBO / atomic / XFB / PBO / IBO bind), inside - // IsBufferDrawClean, and once per SSBO per draw through MarkBufferGpuWritten, where - // the legacy arm paid a pointer compare. HandleOf answers identically - the same - // allocator probe on a miss - and remembers the answer, which the minting overload - // was previously the only writer of. A null answer is deliberately never memoised - // (see the comment at HandleOf). - // P5c merge coordination (CONTRACT-P5C §3.1's named exemption): every caller of - // this function is a site whose handle-carrying records (set_shader_buffers / - // set_stream_output_targets) the client does not emit until sb, so the probe - // below runs inside the scoped exemption. The scope, not a silent guard removal, - // is what keeps the debt named and greppable. - // - // P5e (id), ruling 12: THE SCOPE CHANGED UNDER THIS SITE AND THE SITE HAD TO MOVE. - // The exemption it used to name is now MagmaP7AllocatorDebtScope and is keyed on a - // DirectVulkan server (Magma's four P7 debts); this is Espryt's, and it belongs to - // the frontend-keyed registry family with the rest of the Espryt debt. Which scope - // it names is not cosmetic: the Magma one would not exempt it here at all. It - // retires when vi/sb carry the handle into EnsureBufferResourceForHandle / - // IsBufferDrawCleanByHandle / MarkBufferGpuWrittenByHandle, which is the last - // caller set this function has. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - return g_backendBufferResources.HandleOf(bufferObject); - } - SizeT ResourceWidthForHandle(MG_Pipe::MGPipeHandle res) { return ResourceWidthOf(res); } Uint64 ResourceSerialForHandle(MG_Pipe::MGPipeHandle res) { return ResourceSerialOf(res); } Uint CurrentBufferContextGeneration() { return g_bufferContextGeneration; } - void MarkBufferGpuWritten(const SharedPtr& bufferObject) { - if (!bufferObject) return; - const MG_Pipe::MGPipeHandle res = HandleOfBuffer(bufferObject.get()); - if (MG_Pipe::MGPipeHandleIsNull(res) || MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr) { - // Deliberately NOT a fall-back to MarkGpuWritten: on this arm the resource - // family is switched over, and quietly reaching into the frontend object again - // would hide a missing handle or a missing reverse channel behind a picture - // that still looks right - which is what the subsystem A/B exists to expose. - MGLOG_E_ONCE("MGPipe: no reverse channel for the GPU-write announcement of buffer %u " - "(handle %s, OnGpuWritten %s)", - bufferObject->GetExternalIndex(), - MG_Pipe::MGPipeHandleIsNull(res) ? "missing" : "present", - MG_Pipe::gMGPipeCallbacks.OnGpuWritten == nullptr ? "unset" : "set"); - return; - } - // WHOLE RESOURCE, stated rather than implied: the extent is spelled as one range of - // kMGPipeWholeBuffer rather than as "zero ranges", because a zero count is the - // shape a fully NARROWED announcement will legitimately have once P8/P9 build the - // client's conservative set, and the two must not be the same record. - // - // THE OTHER HALF OF THE CONTRACT, which the range shape alone does not say: what - // this replaces, BufferObject::MarkGpuWritten, sets BOTH m_hasDefinedContent and - // m_gpuWritePending. The client's OnGpuWritten must set both too, or the next - // ResourceRespecify carries HasDefinedContent = 0, Ops_H_Respecify orphans instead - // of uploading, and the shader-written contents are dropped with nothing saying so. - const MG_Pipe::MGPRange whole{0, MG_Pipe::kMGPipeWholeBuffer}; - MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &whole); - } - // See the declaration: re-mints of a live resource's driver id. Written only on // the context thread (all re-mint sites run there), read only by the VAO sync. Uint64 g_bufferBackendIdGeneration = 0; @@ -3893,10 +3744,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return resource->syncedChangeSerial.load(std::memory_order_acquire) == record->Serial; } - Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource) { - return IsBufferDrawCleanByHandle(HandleOfBuffer(frontend), resource, frontend); - } - // The handle arm of the ensure path. Same shape, same order, same branches; the four // frontend reads become the applier's stored descriptor, its Serial and the shadow base // the last content-carrying call handed over. @@ -4146,26 +3993,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return resource; } - GLESBufferResource* EnsureBufferResource(const SharedPtr& bufferObject) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (!bufferObject) return nullptr; - - const MG_Pipe::MGPipeHandle res = HandleOfBuffer(bufferObject.get()); - if (MG_Pipe::MGPipeHandleIsNull(res)) { - // The client never created this resource on the wire. There is deliberately - // no fall-back to the legacy arm: silently twinning it off the frontend - // object would hide a missing emission behind a working picture, which is - // exactly what the subsystem A/B exists to make visible. - MGLOG_E_ONCE("MGPipe: buffer %u has no resource handle - the resource family is switched " - "over but nothing emitted resource_create for it", - bufferObject->GetExternalIndex()); - return nullptr; - } - return EnsureBufferResourceForHandle(bufferObject, res); - } - void BindBufferId(GLenum target, Uint id) { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -5224,17 +5051,6 @@ namespace MobileGL::MG_Backend::DirectGLES { 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). - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside - // the scope - P3b/P4b rekeys the sampler-view registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - return g_backendSamplerViews.HandleOf(textureObject); - } } // namespace SamplerViewImpl namespace VertexArrayImpl { @@ -5361,23 +5177,6 @@ namespace MobileGL::MG_Backend::DirectGLES { BindBackendVAOId(m_backendVAOId); } - inline Bool BindAttributeBuffer(const MG_State::GLState::VertexAttribute& attrib) { - const auto& bufferObject = attrib.Buffer; - if (!bufferObject) { - MGLOG_W_ONCE("Attribute has no bound buffer, skipping."); - return false; - } - - auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer found for attribute's buffer, cannot bind attribute."); - return false; - } - - BufferImpl::BindBufferId(GL_ARRAY_BUFFER, backendResource->id); - return true; - } - // ES 3.1 core. Queried through the loader rather than the version, because the whole // point of using it is to express something the pointer API cannot, and falling back // silently on a driver that lacks it is better than crashing on a null entry point. @@ -5406,41 +5205,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return static_cast(baseInstance) * static_cast(attrib.Stride); } - // Declares one attribute through the ES binding-point API, the only spelling that can - // carry a stride of zero. Returns false when the attribute has no usable buffer, in - // which case nothing was emitted. - inline Bool SyncZeroStrideAttribute(Uint attribIndex, const MG_State::GLState::VertexAttribute& attrib) { - const auto& bufferObject = attrib.Buffer; - if (!bufferObject) { - MGLOG_W_ONCE("Zero-stride attribute %u has no bound buffer, skipping.", attribIndex); - return false; - } - auto* backendResource = BufferImpl::EnsureBufferResource(bufferObject); - if (!backendResource || backendResource->id == 0) { - MGLOG_E_ONCE("No backend buffer for zero-stride attribute %u, cannot bind it.", attribIndex); - return false; - } - - if (!attrib.IsInteger) { - const GLint glSize = attrib.IsBgra ? static_cast(GL_BGRA) : attrib.Size; - g_GLESFuncs.glVertexAttribFormat(attribIndex, glSize, - MG_Util::ConvertDataTypeToGLEnum(attrib.Type), - attrib.Normalized ? GL_TRUE : GL_FALSE, 0); - } else { - g_GLESFuncs.glVertexAttribIFormat(attribIndex, attrib.Size, - MG_Util::ConvertDataTypeToGLEnum(attrib.Type), 0); - } - g_GLESFuncs.glVertexAttribBinding(attribIndex, attribIndex); - // The resolved offset goes on the binding point, not into a relative offset: the - // relative offset is capped by GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET (2047 at - // minimum) while a buffer offset is not, so anything else would break on a large - // one. BindBufferId is bypassed deliberately - glBindVertexBuffer binds into the - // VAO's binding point, not the GL_ARRAY_BUFFER target that cache tracks. - g_GLESFuncs.glBindVertexBuffer(attribIndex, backendResource->id, - static_cast(attrib.Offset), 0); - return true; - } - // ---- the handle arm's small helpers ------------------------------------------- // // Each one is its object-shaped counterpart above with the frontend reads replaced by @@ -6467,162 +6231,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - void BackendTextureObject::RequireImageBindableStorage( - const SharedPtr& stateTextureObject) { - if (m_imageBindableStorageRequired) { - return; - } - // Read before the two flags below schedule the re-mint: whether the driver texture - // already holds this object's levels, and so may hold texels the shadow never saw. - // Also what `trp=` counts on the push builds: a transition that re-mints storage that - // ALREADY EXISTED on the backend. A texture reaching here uninitialised is allocated - // image-bindable up front and replays nothing. - const Bool hadBackendStorage = m_isInitialized && m_backendTextureId != 0 && - m_contextGeneration == g_backendContextGeneration; - m_imageBindableStorageRequired = true; - // ES only needs the storage to be immutable, so an immutable allocation that needs no - // channel widening is already the one a re-mint would produce: keep it, and with it - // every texel the GPU wrote. Pending uploads and real redefinitions still go through - // the normal shape and dirty checks of the next sync. - if (hadBackendStorage && m_backendStorageImmutable && !stateTextureObject->IsTextureView() && - !IsMultisampleTextureTarget(stateTextureObject->GetTarget()) && - !GetImageBindableStorageWidening(stateTextureObject->GetFormat())) { - m_forceTextureParamsResync = true; - return; - } - m_isInitialized = false; - // A view owns no levels. Its sync hands the requirement to the storage texture - // (SyncTextureViewToBackend), whose own transition below re-mints and replays them; a - // re-dirty issued here, through the view, would reach that texture first and claim - // every level for the shadow before the driver's copy could be read back. - if (stateTextureObject->IsTextureView()) { - m_forceTextureParamsResync = true; - return; - } - // Every level this object has ALREADY uploaded has to be replayed, because the - // regeneration this transition schedules re-mints the storage in the image carrier and - // only uploads levels the shadow still calls dirty - which, for a texture that was - // synced before its first glBindImageTexture, is none of them. The new storage would - // come out ALLOCATED AND EMPTY, and every texel the application defined before that - // bind would be gone: the shader reads zeroes and the shadow still holds the data, so - // glGetTexImage (which falls back to the shadow) keeps answering correctly and only - // the image loads are wrong. Reached whenever anything syncs the texture first - a - // glGetTexImage, a draw that samples it, an FBO attach - which is why it survived so - // long: the scenario that binds the image immediately after uploading never sees it. - // - // And the shadow is only the CLIENT's copy. Whatever the GPU wrote into a level - a - // render or clear into it, a copy, a glGenerateMipmap - lives in the driver texture - // alone, so replaying the shadow as it stands put the pre-render bytes (or the zeroes - // of a null-data definition) into the new storage. The driver's levels are therefore - // read back into the shadow first, with the client writes still pending merged over - // them, while the old storage still exists; a level the driver cannot hand back keeps - // the shadow, which is all this used to replay. (This was the known monolith defect of - // W2-REMINT.md §6, the half of ID-P9-8 left to dev; dev's 568090f0 fixed it and P11 M - // merged it.) - // - // THIS IS THE MONOLITH ARM ONLY. Under a transport the re-mint is the server's own - // (RequireImageBindableStorageByHandle: the GPU level read back, the staged store for - // what the client uploaded), and nothing selects this function there - the image - // units sync from the record, the metadata respecify calls the by-handle overload, and - // a frontend object reaching the loop below would meet the layer-1 guard - // (Fatal{RoleViolation, "texture-legacy-arm"}) first. P9 W2 retired the - // `texture-remint-pull` marker this comment used to describe: no pull protocol exists - // because no byte of a re-mint has to come from the client (notes/p9/W2-REMINT.md). - // The two arms now answer the same way: both read the GPU's level back first. - // - // THE COUNT IS TAKEN WHERE A LEVEL IS ACTUALLY REPLAYED, not on entry (review N-4): a - // glBindImageTexture on a texture whose storage is not yet defined reaches here, finds - // no defined level and replays nothing. - // P4a (review M-1): ON THE HANDLE ARM THE RE-DIRTY HAS TO REACH THE APPLIER'S SET, - // not only the frontend's model. The client cleared its own flags when it emitted - // those levels, so MarkStorageDirty below re-arms a model the handle arm does not - // read: the pending set would stay empty, MGB_LEVEL_NEEDS_UPLOAD would answer false - // for every level, and the widened image carrier this transition schedules would be - // allocated EMPTY - the very bug the paragraph above says "survived so long", - // re-introduced one arm over. The dispatch that triggered the bind would read zeroes. - // - // Both models are written, not one: the frontend's because the legacy arm reads it - // and because the client's own NoteLevelDirty hook rides on it, the applier's - // because that is what this arm consumes. - Bool countedRemint = false; - const MG_Pipe::MGPipeHandle rearmRes = - [&]() { - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named - // debt inside the scope - P3b/P4b rekeys the registry. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - return g_backendTextureObjects.HandleOf(stateTextureObject.get()); - }(); - const Uint32 rearmResourceTarget = - MG_Pipe::MGPipeResourceTargetForTextureTarget(stateTextureObject->GetTarget()); - if (MG_Pipe::MGPipeHandleIsNull(rearmRes)) { - MGLOG_E_ONCE("MGPipe: texture %u needs image-bindable storage but has no handle on the " - "handle arm, so the levels it owes cannot be re-armed in the applier's " - "pending set - they would be allocated empty", - stateTextureObject->GetExternalIndex()); - } - if (auto* mipmapObject = MG_State::GLState::AsMipmapTexture(stateTextureObject.get())) { - const auto levelCount = mipmapObject->GetMipmapLevelCount(); - for (const auto& uploadTarget : stateTextureObject->GetUploadTargets()) { - for (Uint level = 0; level < levelCount; ++level) { - const auto levelTexelSize = mipmapObject->GetMipmapTexelSize(uploadTarget, level); - if (levelTexelSize.x() <= 0 || levelTexelSize.y() <= 0) continue; - if (mipmapObject->GetMipmapByteSize(uploadTarget, level) == 0) continue; - // This level's entry in the applier's pending set, read BEFORE the re-arm - // below turns it into a whole-level box: the client writes it still owes - // (see AdoptDriverLevelIntoShadow). Resolved per level - the re-arm may grow - // the record's set and move what an earlier pointer named. - const MG_Pipe::MGPipeResourceRecord* rearmRecord = - MG_Pipe::MGPipeHandleIsNull(rearmRes) ? nullptr : PipeTextureRecordForHandle(rearmRes); - const MG_Pipe::MGPipeResourceRecord::PendingUpload* rearmPending = - rearmRecord != nullptr ? FindPipeTextureUpload(*rearmRecord, static_cast(uploadTarget), - static_cast(level)) - : nullptr; - if (hadBackendStorage && - !AdoptDriverLevelIntoShadow(m_backendTextureId, *this, *stateTextureObject, - *mipmapObject, uploadTarget, level - , - rearmPending - )) { - MGLOG_D("Texture %u level %u: re-minting from the shadow, the driver's copy is not " - "readable here", - stateTextureObject->GetExternalIndex(), level); - } - mipmapObject->MarkStorageDirty(uploadTarget, level, true); - // THE COUNTER BEHIND ROADMAP OPEN QUESTION 2 (final review M-A): once per - // transition that replays a level of storage the backend already held, - // not once per level. - if (!countedRemint) { - countedRemint = true; - if (hadBackendStorage && MG_Util::PipeStats::Enabled()) { - MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureRemintPulls, 1); - } - } - if (!MG_Pipe::MGPipeHandleIsNull(rearmRes)) { - const MG_Pipe::MGPBox wholeLevel{0, - 0, - 0, - static_cast(levelTexelSize.x()), - static_cast(levelTexelSize.y()), - static_cast(std::max(levelTexelSize.z(), 1))}; - RearmPipeTextureLevelUpload( - rearmRes, - MG_Pipe::MGPipePackSubDataTarget(rearmResourceTarget, - static_cast(uploadTarget)), - static_cast(level), wholeLevel); - } - } - } - } - // The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not - // bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels), - // and a widened texture's sampled view has to answer the channels the logical format - // does not have with 0 and 1 - which is a swizzle. The parameter sync is gated on the - // frontend's params version, which this transition does not move, so without the - // override an application that never touched GL_TEXTURE_SWIZZLE_* would keep the - // driver at its defaults and sample the carrier's surplus channels raw. - m_forceTextureParamsResync = true; - } - void BackendTextureObject::RecreateBackendTexture() { if (m_backendTextureId != 0) { ScratchFBOImpl::NoteTextureIdDeleted(m_backendTextureId); @@ -7891,99 +7499,6 @@ namespace MobileGL::MG_Backend::DirectGLES { m_syncedContentVersion = stateTextureObject->GetContentVersion(); } - void BackendTextureObject::SyncTextureViewToBackend( - const SharedPtr& stateTextureObject) { - const auto& storageObject = stateTextureObject->GetViewStorageOwner(); - if (!storageObject) { - MGLOG_E_ONCE("Texture %u claims to be a view but names no storage owner.", - stateTextureObject->GetExternalIndex()); - return; - } - if (!g_GLESCapabilities.SupportsTextureView) { - // Unreachable through the API: the frontend refuses glTextureView with - // GL_INVALID_OPERATION when the backend does not advertise GL_ARB_texture_view, - // and DirectGLES only advertises it when this capability is set. - MGLOG_E_ONCE("Texture view %u reached the backend on a driver without " - "EXT/OES_texture_view.", - stateTextureObject->GetExternalIndex()); - return; - } - - // Deliberately a by-VALUE copy of the SharedPtr: SyncTextureObjectToBackend hands back - // a reference INTO the open-addressed registry map, and the params/sampler syncs below - // (plus any nested growth) can rehash it out from under a reference. - const SharedPtr storageBackendObject = - SyncTextureObjectToBackend(storageObject, m_imageBindableStorageRequired); - if (!storageBackendObject) { - MGLOG_E_ONCE("Failed to sync the storage texture of view %u.", - stateTextureObject->GetExternalIndex()); - return; - } - const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId(); - if (storageBackendTextureId == 0) { - MGLOG_D("Storage texture of view %u has no ES name yet.", - stateTextureObject->GetExternalIndex()); - return; - } - if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) { - StampViewSyncKeys(stateTextureObject); - return; - } - // Either the first sync, or the storage was re-minted underneath us. A name that has - // already been through glTextureView cannot be viewed again, so start from a fresh - // one (this also scrubs the binding caches and bumps the FBO attachment generation). - RecreateBackendTexture(); - - GLenum glInternalFormat = 0; - GLenum glFormat = 0; - GLenum glType = 0; - TextureImpl::GenerateTextureFormatInfo(stateTextureObject->GetFormat(), &glInternalFormat, &glFormat, - &glType, stateTextureObject->GetTarget()); - const GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); - - DebugImpl::ErrorLopper::Clear(); - ResolveTextureViewEntryPoint()(m_backendTextureId, target, storageBackendTextureId, glInternalFormat, - stateTextureObject->GetViewMinLevel(), - stateTextureObject->GetViewNumLevels(), - stateTextureObject->GetViewMinLayer(), - stateTextureObject->GetViewNumLayers()); - const GLenum error = g_GLESFuncs.glGetError(); - if (error != GL_NO_ERROR) { - MGLOG_E_ONCE("glTextureView(view=%u target=%s origtexture=%u internalformat=%s levels=[%u,%u) " - "layers=[%u,%u)) failed: %s", - m_backendTextureId, MG_Util::ConvertGLEnumToString(target).c_str(), - storageBackendTextureId, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str(), - stateTextureObject->GetViewMinLevel(), - stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(), - stateTextureObject->GetViewMinLayer(), - stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers(), - MG_Util::ConvertGLEnumToString(error).c_str()); - return; - } - - m_viewSourceBackendTextureId = storageBackendTextureId; - m_isInitialized = true; - // A view's storage is immutable by construction (its origtexture had to be), which is - // what keeps the respecify paths away from this name. - m_backendStorageImmutable = true; - const auto baseSize = stateTextureObject->GetBaseSize(); - m_prevTextureInfo = {stateTextureObject->GetFormat(), - static_cast(baseSize.x()), - static_cast(baseSize.y()), - static_cast(baseSize.z()), - static_cast(stateTextureObject->GetViewNumLevels()), - 0, - stateTextureObject->GetSamples(), - stateTextureObject->HasFixedSampleLocations()}; - MGLOG_D("Texture view %u (ES %u) now views storage texture %u (ES %u), levels [%u,%u) layers [%u,%u)", - stateTextureObject->GetExternalIndex(), m_backendTextureId, storageObject->GetExternalIndex(), - storageBackendTextureId, stateTextureObject->GetViewMinLevel(), - stateTextureObject->GetViewMinLevel() + stateTextureObject->GetViewNumLevels(), - stateTextureObject->GetViewMinLayer(), - stateTextureObject->GetViewMinLayer() + stateTextureObject->GetViewNumLayers()); - StampViewSyncKeys(stateTextureObject); - } - void BackendTextureObject::SyncMipmapsToBackend( const SharedPtr& stateTextureObject) { // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; @@ -8052,17 +7567,6 @@ namespace MobileGL::MG_Backend::DirectGLES { SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject); return; } - if (pushedStorage != nullptr) { - // Not a view: fall through to the storage body with the frontend test skipped. - } else - // A texture created by glTextureView owns no storage: the levels, the format and - // every texel belong to the texture it views, and this name only has to be made to - // ALIAS them. Everything below - storage allocation, respecification, per-level - // uploads - would be re-doing the storage texture's work on the wrong name. - if (stateTextureObject->IsTextureView()) { - SyncTextureViewToBackend(stateTextureObject); - return; - } #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); @@ -9377,8 +8881,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // Need to sync texture buffer if not synced yet auto* backendBufferResource = - pushedStorage != nullptr ? BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer) - : BufferImpl::EnsureBufferResource(buffer); + BufferImpl::EnsureBufferResourceForHandle(buffer, pushedTexBuffer); if (!backendBufferResource || backendBufferResource->id == 0) { MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u", MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); @@ -9579,22 +9082,14 @@ namespace MobileGL::MG_Backend::DirectGLES { #undef MGB_LEVEL_TEXELS #undef MGB_STAGED_TEXTURE_LIVE - // P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD. On the by-handle arm the handle - // is already known - the caller resolved the record before it resolved this twin and - // noted the handle on the twin itself - so the client allocator is not probed at all. - // What is left below is the MONOLITH GLUE half (HandleOfBuffer's shape): a twin minted - // from a frontend object and never adopted by handle still re-derives one. + // P5e (tx2), CONTRACT-P5E §5.2: THIS TWIN'S OWN RECORD, by the handle the caller noted on + // the twin before it resolved it; the client allocator is never probed. const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolveOwnRecord( const SharedPtr& stateTextureObject) const { - MG_Pipe::MGPipeHandle res = m_pushedSyncHandle; - if (MG_Pipe::MGPipeHandleIsNull(res)) { - if (!stateTextureObject) return nullptr; - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the - // scope - the by-handle arm above is what retires it. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - res = g_backendTextureObjects.HandleOf(stateTextureObject.get()); - } - return PipeTextureRecordForHandle(res); + (void)stateTextureObject; + // P13: a twin is only ever reached by handle, and the handle is noted on it first. + if (MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle)) return nullptr; + return PipeTextureRecordForHandle(m_pushedSyncHandle); } const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler( @@ -10143,21 +9638,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } #endif - void NoteDriverSideTextureWrite(const SharedPtr& textureObject) { - SharedPtr storage = textureObject; - // Bounded: GL resolves a view of a view to the original storage, so this takes one step. - for (Uint hop = 0; storage && storage->IsTextureView() && hop < 4; ++hop) { - storage = storage->GetViewStorageOwner(); - } - if (!storage) return; - auto* slot = g_backendTextureObjects.Find(storage.get()); - auto& backendObj = slot ? *slot : g_backendTextureObjects.GetOrCreate(storage); - if (!backendObj) { - backendObj = MakeShared(); - } - backendObj->NoteDriverSideWrite(); - } - // P5e (id), CONTRACT-P5E §4.1. See Managers.h for the contract. BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res) { if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; @@ -14236,40 +13716,10 @@ namespace MobileGL::MG_Backend::DirectGLES { m_syncedSamplerSerial = record->Serial; pushedParams = &record->Params; } else { - // NO HANDLE CARRIED. Two shapes, and they are told apart by whether this - // object is in the twin registry at all: - // - // * not registered -> a sampler this BACKEND minted for its own use (the - // raw-depth-fetch sampler, DirectGLES.cpp:207-218). The client has never - // seen it, no record can exist for it now or after any package lands, - // and the object IS the authority for server-owned state. Not a seam. - // * registered -> an application sampler whose caller did not carry the - // unit's handle. That is the E-side call-site gap; it is named once and - // the values are taken from the object, which in monolith are the very - // values the client content-addressed, so the picture stays right while - // the gap is visible rather than silent. - // P5c (G6, CONTRACT-P5C §5.4): the registration probe below resolves the - // sampler's handle by frontend identity - frontend-keyed twin resolution, - // named debt inside the scope - P3b/P4b rekeys the registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - if (!MG_Pipe::MGPipeHandleIsNull( - g_backendSamplerObjects.HandleOf(stateSamplerObject.get()))) { - MGLOG_E_ONCE("MGPipe: sampler %u was synced without its unit's SamplerCso handle, so " - "the content-addressed record cannot be named and the object's own " - "parameters are used - the caller must pass " - "MGPipeApplier().BoundSamplerStates[unit]", - stateSamplerObject->GetExternalIndex()); - } - // Spelled exactly as the pre-handle arm below spells it (compare widened, - // assign narrowed) so the two cannot drift on a version past 65535. - const Uint currentSamplerVersion = stateSamplerObject->GetVersion(); - if (m_isInitialized && m_syncedSamplerVersion == currentSamplerVersion) { - MGLOG_D("Sampler parameters have not changed for sampler ID: %u, skipping sync.", - stateSamplerObject->GetExternalIndex()); - return; - } - m_syncedSamplerVersion = currentSamplerVersion; - pushedParams = &stateSamplerObject->GetAllSamplerParameters(); + // P13: every sampler twin is a CSO twin, synced with its handle + // (ResolveSamplerCsoTwin); a sampler with no record has nothing to push. + MGLOG_E_ONCE("MGPipe: sampler %u was synced without a SamplerCso handle", samplerName); + return; } MGLOG_D("Syncing sampler with backend ID %u to backend for state ID %u", m_backendSamplerId, samplerName); @@ -14474,122 +13924,6 @@ namespace MobileGL::MG_Backend::DirectGLES { #define MGB_RBO_SAMPLES \ (pushedRecord != nullptr ? static_cast(pushedRecord->Desc.Samples) : static_cast(stateRBOObject->GetSamples())) - void BackendRenderbufferObject::SyncToBackend( - const SharedPtr& stateRBOObject) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (!stateRBOObject) { - MGLOG_E_ONCE("State RBO object is null, cannot sync to backend."); - return; - } - - MGLOG_D("Syncing RBO with backend ID %u to backend for state ID %u", m_backendRBOId, - stateRBOObject->GetExternalIndex()); - - const MG_Pipe::MGPipeResourceRecord* pushedRecord = nullptr; - if (TextureResourceSubsystemEnabled()) { - // P5c (G6, CONTRACT-P5C §5.4): the renderbuffer's handle is resolved by - // frontend identity below - frontend-keyed twin resolution, named debt inside - // the scope - P3b/P4b rekeys the registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - // MONOLITH GLUE, named as such: the Renderbuffer handle of an object this - // backend still arrives holding. Under a real split it rides in the payload. - const MG_Pipe::MGPipeHandle res = g_backendRenderbufferObjects.HandleOf(stateRBOObject.get()); - pushedRecord = PipeRenderbufferRecordForHandle(res); - if (pushedRecord == nullptr) { - MGLOG_E_ONCE("MGPipe: renderbuffer %u has no applier record on the handle arm, so " - "its storage cannot be allocated from the pushed descriptor " - "(handle {%u, %u})", - stateRBOObject->GetExternalIndex(), res.Slot, res.Gen); - return; - } - if (m_isInitialized && m_syncedResourceSerial != 0 && - m_syncedResourceSerial == pushedRecord->Serial) { - MGLOG_D("RBO %u already initialized with matching parameters, skipping re-allocation.", - stateRBOObject->GetExternalIndex()); - return; - } - } else { - // UNREACHABLE: ResolveTextureResourceSubsystemArm stops at its first call when - // the bit is clear and the pre-handle arm is not compiled. Kept, and kept loud. - MGLOG_E_ONCE("MGPipe: the texture-resource subsystem bit is clear and " - "MOBILEGL_PIPE_LEGACY_MEMOS=0 removed the pre-handle renderbuffer " - "four-field cache, so this configuration has no arm at all"); - return; - } - - // A RE-STORAGE IS A REDEFINITION ON THE SAME DRIVER ID (P4a fable seam F-3, the - // pre-handle half): glRenderbufferStorage below re-allocates behind the name the - // framebuffer twins already attached, the frontend's renderbuffer setters bump no - // version and no framebuffer version sees them, so without this the FBO memo kept - // the widening masks of the storage the renderbuffer was attached with. Same - // generation a texture re-mint takes, for the same reason; the first allocation is - // not a redefinition. - // PUSH BUILDS ONLY, for the texture twin's reason (G1: the pull library stays - // byte-identical to the P4a baseline). - if (m_isInitialized) { - ++FramebufferImpl::g_attachmentBackendIdGeneration; - } - - Bind(); - - // Allocate storage - TextureInternalFormat internalFormat = MGB_RBO_FORMAT; - Int width = MGB_RBO_WIDTH; - Int height = MGB_RBO_HEIGHT; - Int samples = MGB_RBO_SAMPLES; - GLenum glInternalFormat, glType, glFormat; - TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType); - - // The allocation is deferred to here, so an ES driver that refuses it (a - // multi-gigabyte renderbuffer is refused routinely) used to leave m_isInitialized - // true over a renderbuffer with no storage and say nothing at all: the attachment - // then rendered nowhere. Drain first so the check cannot pick up an unrelated stale - // flag, and report GL_OUT_OF_MEMORY to the application. The error lands on whatever - // entry point triggered the sync rather than on glRenderbufferStorage itself, which - // is where the deferred model puts it - still far better than silence. - DebugImpl::ErrorLopper::Clear(); - if (samples > 0) { - // Same clamp as the multisample texture path: the frontend accepts the count it - // advertised, the driver only takes the count it supports for this format, and - // the state object keeps reporting the requested one. - const auto backendSamples = static_cast(ClampSamplesToBackendSupport( - GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples)); - g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat, - static_cast(width), - static_cast(height)); - } else { - g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast(width), - static_cast(height)); - } - if (g_GLESFuncs.glGetError() == GL_OUT_OF_MEMORY) { - MGLOG_E_ONCE("Renderbuffer %u storage allocation ran out of memory: %dx%d, samples=%d, format=%s", - stateRBOObject->GetExternalIndex(), width, height, samples, - MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); - if (MG_Pipe::gPipeInputs.IsLive()) { - MG_Pipe::gPipeInputs.RecordError( - ErrorCode::OutOfMemory, - MakeUnique("DirectGLES", "BackendRenderbufferObject::SyncToBackend", - "The ES driver could not allocate the renderbuffer storage.")); - } - } - DebugImpl::ErrorLopper::Clear(); - - m_cacheInternalFormat = internalFormat; - m_cacheWidth = width; - m_cacheHeight = height; - m_cacheSamples = samples; - // Stamped in the same breath as the four cache members and AFTER the allocation, so - // an allocation the driver refused with GL_OUT_OF_MEMORY above leaves the twin - // describing what it actually holds. The deferred OOM report and its RecordError are - // untouched: the error still lands on whatever entry point triggered the sync, which - // is where the deferred model puts it. - if (pushedRecord != nullptr) m_syncedResourceSerial = pushedRecord->Serial; - - m_isInitialized = true; - MGLOG_D("RBO %u sync completed. backend ID %u", stateRBOObject->GetExternalIndex(), m_backendRBOId); - } #undef MGB_RBO_FORMAT #undef MGB_RBO_WIDTH #undef MGB_RBO_HEIGHT diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 6fb04298..4851a3a0 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -287,19 +287,12 @@ namespace MobileGL::MG_Backend::DirectGLES { EndViewportRoutingPasses(passCount); } - // The backend twin table. Two arms live behind this one interface (ARCHITECTURE.md 9.6 - - // after Track H the MOBILEGL_PIPE_PUSH bitmap alone is not a valid A/B, because with a bit - // clear the backend would still be running the re-keyed code): - // - // legacy (MOBILEGL_PIPE_LEGACY_MEMOS): UnorderedMap keyed on the - // frontend heap ADDRESS, with a weak_ptr per entry as the ABA defence, an erase - // inside Find, and a garbage sweep as the only death signal. Pre-P2 code verbatim. - // handles (MOBILEGL_PIPE_PUSH and kMGPipeSubsystemEsprytSlots): BackendSlotTable, keyed - // on MGPipeHandle{Slot, Gen}. See SlotTables.h for what that buys. - // - // Which arm runs is fixed once per process (EsprytSlotTablesEnabled()): the two arms hold - // their twins in different containers, so a mid-run flip would strand every twin already - // built. Every call site below this class is arm-agnostic and unchanged. + // The backend twin table: BackendSlotTable buckets keyed on MGPipeHandle{Slot, Gen} (see + // SlotTables.h for what that buys). P13 deleted the legacy arm - an UnorderedMap keyed on + // the frontend heap ADDRESS with a garbage sweep as its only death signal - together with + // every frontend-keyed entry (GetOrCreate(object), Find(object), HandleOf, the per-handle + // state note, the lifetime-id death walk): a twin is resolved only by the handle a record + // carried. // // The kind is a template parameter ONLY in the push build. G1 requires the pull build's // symbol set to be byte-for-byte the pre-P2 one, and a third template argument changes @@ -315,21 +308,7 @@ namespace MobileGL::MG_Backend::DirectGLES { class StateBackendObjectRegistry { public: - using StatePtr = SharedPtr; - using StateWeakPtr = std::weak_ptr; using BackendPtr = SharedPtr; - - // The backend twin and the weak reference that decides whether the raw key still - // names the state object the twin was built for. Both live in one entry: a - // separate liveness map answered nothing the backend probe had not already found - // and cost a second hash lookup on every Find, which the draw path runs ~10 times. - struct Entry { - BackendPtr backend; - StateWeakPtr stateRef; - }; - using BackendMap = UnorderedMap; - using iterator = typename BackendMap::iterator; - using const_iterator = typename BackendMap::const_iterator; using SlotTable = BackendSlotTable; // P14 S6: the ONE bucket of this kind the calling thread is in. Every handle-keyed entry // below resolves through it, so the six twin registries answer per {session, share @@ -337,117 +316,6 @@ namespace MobileGL::MG_Backend::DirectGLES { using SlotTables = SlotTableRegistry; using TwinKey = DirectGLES::TwinKey; - BackendPtr& GetOrCreate(const StatePtr& stateObj) { -#if MOBILEGL_BUILD_DISAGGREGATED - // Check before arm selection: disabling slot tables must not expose the - // legacy raw-pointer registry on a transport apply thread. - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.GetOrCreate(StatePtr)"); -#endif - MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null"); - - if (EsprytSlotTablesEnabled()) { - // The slot table arms the teardown sentinel itself, at its own first - // insertion (D13; SlotTables.h) - so a table used outside a registry arms - // it too, which is right: it is the twin, not the registry, that owns the - // driver id a guarded destructor exists for. - return m_slotTables.ForCallingThread().GetOrCreate(stateObj); - } - // Twin creation is the moment a driver-owned id starts needing a guarded - // destructor; cold path, so the once-guard costs nothing per draw. It is armed - // here, at the first insertion - a destructor hook on the table itself is wrong - // for the reason spelled out above InProcessTeardown(). - EnsureProcessTeardownSentinel(); - // Sweep BEFORE the entry reference below exists: the map is open-addressed and an - // erase relocates the rest of the probe cluster, so collecting once that reference - // is taken would invalidate it. The sweep is therefore owed from an earlier call - // rather than triggered by this one. - if (m_creationTick >= kCreationGCInterval) { - m_creationTick = 0; - CollectGarbage(); - } - const SizeT entryCountBeforeInsert = m_entries.size(); - auto& entry = m_entries[stateObj.get()]; - if (m_entries.size() != entryCountBeforeInsert) { - // A key the registry has never held. Nothing tells the backend that a texture or - // renderbuffer was DELETED - the twin, and the driver storage it owns, lives - // until a collection - and CollectGarbageIfNeeded is ticked only from the - // per-draw sync paths, which a CTS-shaped workload runs about ten times per - // case. 1024 of those ticks then span ~100 cases, so ~100 cases' worth of dead - // (and, for this suite, gigabyte-sized) objects stay allocated at once. Object - // CHURN rather than draw count is what makes the sweep urgent, so a twin the - // registry has never seen ticks it too - and it does so on the path that is - // about to allocate, which is exactly when the memory is needed. - ++m_creationTick; - } - if (entry.stateRef.expired()) { - // The previous owner of this address is gone and the allocator handed it - // to a new object: its twin describes ids the new state object never made. - entry.backend.reset(); - } - entry.stateRef = stateObj; - return entry.backend; - } - - // Null when no live state object owns this key. - // - // On the HANDLE arm the result is a stable array element: only a GetOrCreate that grows - // the table can move it, and nothing else on the table invalidates it. - // - // On the LEGACY arm the result points into the map, so it stays valid only until the - // next GetOrCreate/Find/CollectGarbage on this registry. Take that literally, including - // for Find: the map is open-addressed and erases by shifting the rest of the probe - // cluster into the hole, so an erase relocates entries OTHER than the erased one - and - // Find erases, whenever it lands on a key whose state object has expired. Callers that - // need the twin across another registry call must copy the BackendPtr out (or keep only - // the pointee, which is heap-allocated and never moves). - BackendPtr* Find(StateObject* stateObj) { -#if MOBILEGL_BUILD_DISAGGREGATED - // Check before arm selection: disabling slot tables must not expose the - // legacy raw-pointer registry on a transport apply thread. - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.Find(StateObject*)"); -#endif - if (EsprytSlotTablesEnabled()) { - return m_slotTables.ForCallingThread().Find(stateObj); - } - const auto entryIt = m_entries.find(stateObj); - if (entryIt == m_entries.end()) { - return nullptr; - } - if (entryIt->second.stateRef.expired()) { - m_entries.erase(entryIt); - return nullptr; - } - return &entryIt->second.backend; - } - - const BackendPtr* Find(StateObject* stateObj) const { - return const_cast(this)->Find(stateObj); - } - - iterator begin() { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.begin"); -#endif - return m_entries.begin(); - } - const_iterator begin() const { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("Registry.begin"); -#endif - return m_entries.begin(); - } - iterator end() { return m_entries.end(); } - const_iterator end() const { return m_entries.end(); } - - // The {slot, gen} this object's twin is keyed on, or the null handle. This is what a - // backend memo stores instead of a raw pointer, a GL name or a bare lifetime id. - MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const { - if (EsprytSlotTablesEnabled()) { - return m_slotTables.HandleOf(stateObj); - } - return MG_Pipe::kMGPipeNullHandle; - } - // The twin at a handle, or null when the slot is free or its Gen has moved on. This is // the lookup a backend memo that already holds a handle wants: no lifetime-id probe. BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) { @@ -503,27 +371,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return m_slotTables.LiveGenAt(slot); } - // P5c (hd): the two halves of SlotTables.h's state note, forwarded. A caller holding - // both the record's handle and the frontend object (the record-driven sync) notes the - // object so a later handle-only resolution can reach it without the client allocator. - // - // P5e (id): under MOBILEGL_PIPE_PUSH rather than MOBILEGL_BUILD_DISAGGREGATED, because - // the re-typed ForEachLive's caller resolves its object through StateForHandle in the - // push-monolith build too. Each half is a named Fatal from an unbarriered apply - // (SlotTables.h); on the legacy arm there is no note and StateForHandle answers null, - // which is the answer the legacy walk's own weak_ptr test already gives. - void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) { - if (EsprytSlotTablesEnabled()) { - m_slotTables.NoteStateForHandle(handle, stateObj); - } - } - StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const { - if (EsprytSlotTablesEnabled()) { - return m_slotTables.StateForHandle(handle); - } - return nullptr; - } - // NO ReleaseByHandle HERE THROUGH P5, AND THAT WAS A DECISION (review M-4). The death // half of GetOrCreateByHandle existed only for a kind whose announcement is its own // destroy CALL rather than the shared death notice - which was the BUFFER family @@ -549,24 +396,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } - // 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 - // (SlotTables.h explains the holder list and why one holder was a leak). - // - // The legacy arm cannot answer this at all - its key is the frontend heap ADDRESS and - // the object is already gone by the time the notice arrives - so there it is a no-op - // and the garbage sweep stays its only death signal. That asymmetry is not an - // oversight: it is the A/B the compile-time arm exists to make measurable - // (ARCHITECTURE.md 9.6), and announced-versus-discovered death is one of the things - // being measured. - static Bool DestroyByLifetimeId(Uint64 lifetimeId) { - if (EsprytSlotTablesEnabled()) { - return SlotTable::OnFrontendObjectDestroyed(lifetimeId); - } - return false; - } - // P5e (id), CONTRACT-P5E §4.1: fn(MGPipeHandle, const BackendPtr& twin) over every live // entry of the HANDLE ARM, and of that arm only. // @@ -628,61 +457,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (EsprytSlotTablesEnabled()) m_slotTables.ForEachBucket(fn); } - // The seven DirectGLES.cpp call sites drive the LEGACY arm and nothing else. On the - // handle arm death is announced by the frontend object's destructor - // (MG_State/GLState/StateObjectDeathNotice.h), so there is no garbage to collect on a - // tick, the slot table has no collector to forward to, and this is the predicted - // branch plus a return - which is how ROADMAP.md:18's "delete the GC" is delivered - // without deleting the legacy arm's own collector while that arm is still compiled - // beside it. - void CollectGarbageIfNeeded() { - if (EsprytSlotTablesEnabled()) { - return; - } - } - - // Pre-P2 API, kept for the legacy arm. On the handle arm there is nothing it could - // collect: a twin leaves with its object's death notice, and a notice dropped during - // process teardown is a deliberate leak (SlotTables.h), not garbage awaiting a call. - void CollectGarbageNow() { - if (EsprytSlotTablesEnabled()) { - return; - } - CollectGarbage(); - } - private: - void CollectGarbage() { - if (m_isCollecting) { - return; - } - - m_isCollecting = true; - - Vector staleKeys; - staleKeys.reserve(m_entries.size()); - for (const auto& [stateKey, entry] : m_entries) { - if (entry.stateRef.expired()) { - staleKeys.push_back(stateKey); - } - } - - for (auto* stateKey : staleKeys) { - m_entries.erase(stateKey); - } - - m_isCollecting = false; - } - - private: - static constexpr Uint32 kGCInterval = 1024; - // Creations are far rarer than draws, so this counts in a much smaller unit than - // kGCInterval does. - static constexpr Uint32 kCreationGCInterval = 64; - BackendMap m_entries; - Uint32 m_gcTick = 0; - Uint32 m_creationTick = 0; - Bool m_isCollecting = false; // P14 S6: the ONE bucket per {session, share group}, hidden behind the same member names // the single table had, so every handle-keyed call site above and every caller below is // unchanged. See SlotTables.h's SlotTableRegistry for the resolution chain. @@ -1194,15 +969,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // to answer it differently. Bool ResourceContentIsDeclared(MG_Pipe::MGPipeHandle res); - // MONOLITH GLUE, and named as such: the handle of a resource this backend is looking - // at through a frontend object, resolved through the client allocator's lifetime-id - // index. Every caller is a site P3a deliberately does NOT migrate - the SSBO / UBO / - // indirect / pack-PBO binding walks are dirty bits 15-17 and P4b's, and the index - // host mirror is P8's - so they still arrive holding the object. Under a real split - // neither the object nor its lifetime id exists on this side and every one of them - // has to receive the handle in a payload instead. - MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject); - // TIER 1 OF THE THREE-TIER FLUSH LADDER, AS A PURE FUNCTION (P5 b1). // // `GL_MAP_INVALIDATE_RANGE_BIT` is not a hint, it is an ASSERTION THAT THE OLD BYTES @@ -1267,18 +1033,6 @@ namespace MobileGL::MG_Backend::DirectGLES { GLESBufferResource* EnsureBufferResourceForHandle( const SharedPtr& bufferObject, MG_Pipe::MGPipeHandle res); - // P3a (D-D): "the GPU wrote through this resource", announced on the reverse channel - // instead of poked into the frontend object. ARCHITECTURE.md calls OnGpuWritten a - // NARROWING channel - the client builds its pending set conservatively at each - // draw/dispatch emission point and this callback only ever takes entries out of it - - // so in P3a, where the client's conservative set is exactly what the three - // MarkGpuWritten sites marked, the announced set is the whole resource and the - // observable behaviour is identical. P8/P9 narrow it; the channel is what they need. - // - // The legacy arm keeps calling BufferObject::MarkGpuWritten directly, and the pull - // build never sees this function at all (G1). - void MarkBufferGpuWritten(const SharedPtr& bufferObject); - // The applier's stored extent for this resource, 0 when it has no record. The one // thing outside BufferImpl that needs it is the fp64 narrowing, whose source extent // used to be BufferObject::GetSize(). @@ -1305,25 +1059,8 @@ namespace MobileGL::MG_Backend::DirectGLES { void OnServerSessionEndedWithOthersLive(); void OnBackendContextDestroyed(); - // Get-or-create the backend resource and bring its storage up to date - // (creates the GL buffer, replays pending ops, pushes persistent-mapped - // ranges). Requires the ES context to be current. Returns nullptr only - // for null input. - GLESBufferResource* EnsureBufferResource(const SharedPtr& bufferObject); // Existing resource or nullptr; performs no GL calls. GLESBufferResource* GetBufferResource(MG_State::GLState::BufferObject* bufferObject); - // True when EnsureBufferResource(frontend) would provably fall straight through - // every branch and do no work — i.e. `resource` is still the frontend's own - // resource, its id belongs to the live ES context, and either it is the - // zero-copy coherent persistent store (draw-time sync is a no-op by design) or - // the storage is initialized at the right size with no pending ops and a synced - // change serial while the buffer is not mapped (an active map may owe a - // per-draw persistent-range push, so it always takes the full path). - // `frontend` must be non-null and alive; the caller guarantees that by holding - // (or shadowing something that holds) a SharedPtr to it. Enables the per-VAO - // resolved-buffers memo to skip EnsureBufferResource on clean static buffers. - Bool IsBufferDrawClean(const MG_State::GLState::BufferObject* frontend, const GLESBufferResource* resource); - // Deletes GL buffers whose owning frontend objects died (possibly on a // thread without a current ES context). Called from draw-time sync. void ProcessDeferredBufferReleases(); @@ -1888,7 +1625,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // attachment sync and the image sweep both call it, which is why it is declared // once here rather than twice in two packages' worktrees. void SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture); - void SyncTextureViewToBackend(const SharedPtr& stateTextureObject); void StampViewSyncKeys(const SharedPtr& stateTextureObject); // P5e (tx2). THE VIEW ARM UNDER A TRANSPORT, and what it can and cannot answer. // @@ -1911,12 +1647,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // parameter and sampler halves are unchanged and run on this name as on any other. void SyncBuiltinSamplerToBackend(const SharedPtr& stateTextureObject); void SyncTextureParamsToBackend(const SharedPtr& stateTextureObject); - // Marks the texture as one whose ES storage has to be image-bindable, which for a - // non-core image format means re-minting it in the widening's carrier. Takes the state - // object because the levels already uploaded have to be marked dirty again: the - // re-mint allocates fresh storage and only replays what the shadow still calls dirty. - void RequireImageBindableStorage( - const SharedPtr& stateTextureObject); // Server-owned promotion: preserve an already image-bindable native // allocation, or capture the old native contents, merge pending // upload regions and re-arm the server's whole-level replay set. @@ -2252,14 +1982,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // table's answer is an array element and only a GetOrCreate that GROWS the table moves // it, which this function has already done by the time it returns. BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle res); - SharedPtr& SyncTextureObjectToBackend( - const SharedPtr& textureObject, - Bool imageBindableStorageRequired = false); - // Every path that lets the DRIVER write a texture's texels calls this (see - // BackendTextureObject::m_driverMayHoldGpuWrites): an application framebuffer attachment, - // a writable image binding, a copy, blit or mipmap generation into it. A view writes its - // storage texture's image, so the storage texture is the one marked. - void NoteDriverSideTextureWrite(const SharedPtr& textureObject); // ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by // id/tx2) -------------------------------------------------------------------------- // @@ -3490,14 +3212,6 @@ namespace MobileGL::MG_Backend::DirectGLES { 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 namespace RenderbufferImpl { @@ -3509,7 +3223,6 @@ namespace MobileGL::MG_Backend::DirectGLES { ~BackendRenderbufferObject(); BackendRenderbufferObject(const BackendRenderbufferObject&) = delete; BackendRenderbufferObject& operator=(const BackendRenderbufferObject&) = delete; - void SyncToBackend(const SharedPtr& stateRBOObject); // P5e SEAM (declared by c0e, bodied by fb): the renderbuffer twin of the // framebuffer overload above. MGPSurface::Res names the renderbuffer and the // resource record already carries its format, extent and sample count (P4a), so the diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h index 6fb906c5..9c12f9f1 100644 --- a/MobileGL/MG_Backend/DirectGLES/SlotTables.h +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -17,7 +17,6 @@ // translation units that include it without copying Managers.h's include order. #include "DirectGLES.h" -#include #if MOBILEGL_BUILD_DISAGGREGATED #include #endif @@ -43,63 +42,32 @@ // Death is ANNOUNCED, and that is what lets this table have no garbage collector - the // deliverable ROADMAP.md:18 spells "GC" in and the one D13 makes a precondition of the switch- // over. All six re-keyed object classes raise MG_State::GLState::NotifyStateObjectDestroyed() -// from their destructor (BufferBackendOps' shape, one entry point for six kinds), the backend -// consumes it in Managers.cpp, and OnFrontendObjectDestroyed() below drops the twin in EVERY -// table of the kind and returns the slot, at the moment the frontend object's last SharedPtr -// goes. So: -// * there is NO draw-path tick, NO creation tick and NO sweep of any kind on this arm. The -// seven CollectGarbageIfNeeded call sites in DirectGLES.cpp drive the LEGACY registry only; +// from their destructor carrying {kind, lifetimeId, handle}; the backend consumes it in +// Managers.cpp and releases the twin BY THAT HANDLE in every holder of the kind +// (ReleaseTwinByHandle below - the same release the wire's object_death runs on a server), and +// the CLIENT returns the slot after the notice (PipeFill.cpp's NotifyAndFree). So: +// * there is NO draw-path tick, NO creation tick and NO sweep of any kind; // * a twin, and the driver storage it owns, is freed when the application lets go of the // object rather than up to 64 creations or 1024 draw ticks later. That is what // Managers.h's "dead gigabytes" note asked for. // // EVERY HOLDER OF THE KIND, not one. Two live tables of one kind is a real configuration - the // ScopedDirectGLESTextureBindings fixture keeps a by-value copy of the Texture registry for the -// length of a test, and a context reset does the same in reverse - and the slot allocator -// erases its lifetimeId -> slot mapping on Free, so a notice delivered to one holder and -// resolved again by the next would find nothing to resolve. Every table therefore links itself -// into a per-table-type list at construction and out at destruction, and one notice resolves -// the handle ONCE, drops the twin in each holder BY HANDLE, and frees the slot once, last. No -// holder can be left naming a live entry for a dead object, and there is nothing a sweep could -// still find. (The list is per table TYPE; the kind is the type's template parameter, and each -// of the six kinds has exactly one table type in this backend. Magma's subsystem-4 table mints -// out of its own per-renderer allocator, not MGPipeSlots(), so it is not a holder here.) +// length of a test, and a context reset does the same in reverse. Every table therefore links +// itself into a per-table-type list at construction and out at destruction, and one release +// drops the twin in each holder by handle. (The list is per table TYPE; the kind is the type's +// template parameter, and each of the six kinds has exactly one table type in this backend.) // -// The weak_ptr per entry survives as the MONOLITH-GLUE half of the table's identity: the -// minting GetOrCreate stores it, NoteStateForHandle/StateForHandle read it, and the legacy -// death notice walks it. It is never an identity test - that is what Gen is for - and it is -// never read to decide whether an entry is dead: a destructor that runs after exit() has begun -// has its notice dropped by InProcessTeardown(), and that twin is then a DELIBERATE leak (the -// process is exiting, the driver reclaims the object, and a twin destructor must not call into -// a driver that may already be unloaded), not something to be collected later. +// A destructor that runs after exit() has begun has its notice dropped by InProcessTeardown(), +// and that twin is then a DELIBERATE leak (the process is exiting, the driver reclaims the +// object, and a twin destructor must not call into a driver that may already be unloaded). // -// P5e (id, CONTRACT-P5E §4.1): ForEachLive() NO LONGER HANDS IT OUT. It answers -// fn(MGPipeHandle, const BackendPtr&), because a walk that produced a frontend SharedPtr out -// of server memory was the last place the server could hold one across records - and a caller -// that still needs the object (the detach walk) asks StateForHandle for it BY HANDLE, inside -// the scope that names the debt, so the read is one greppable site rather than a property of -// the iteration. -// -// P3+ DEBT, recorded rather than hidden: this header is under MG_Backend/ and it MINTS -// handles (MGPipeSlots().Acquire below) off a frontend SharedPtr's GetLifetimeId(). -// MGPipeHandles.h:13-16 says a handle is minted by the CLIENT and never by the server, and -// under a real split neither the frontend object nor its lifetime id exists on this side of -// the wire. This is monolith glue: the minting and the lifetimeId -> handle resolution both -// belong on the client, and the backend should receive the handle in the verb payload. It is -// NOT part of "Track H done" and check_include_closure.py does not probe MG_Backend headers, -// so nothing catches it automatically. -// -// P5c (hd, CONTRACT-P5C §3.1) is what gives the debt teeth: with an active transport the -// minting GetOrCreate, HandleOf and OnFrontendObjectDestroyed raise -// Fatal{RoleViolation, "MGPipeSlots"} when reached from the apply thread (the same check the -// allocator's own three entries carry, repeated here so the refusal names this surface), and -// the split paths resolve through GetOrCreate(handle) / ReleaseByHandle instead. -// -// P5f (fr): the frontend-keyed half survives only as MONOLITH GLUE. HandleOf, -// GetOrCreate(StatePtr), NoteStateForHandle and StateForHandle refuse EVERY transport -// apply before touching a frontend object, with or without a barrier or named scope. -// Find(StateObject*) delegates to HandleOf; handle-only iteration never reads stateRef. -// The registry guard is independent of Magma's legacy allocator debt scope. +// P13: THE TABLE IS HANDLE-ONLY. The frontend-keyed half - a GetOrCreate that minted handles +// off a frontend object's lifetime id from inside MG_Backend, the object-keyed Find / HandleOf +// with their lifetime-id memo, the weak per-entry state note and the lifetime-id death walk - +// was monolith glue, unreachable once every family ran the record arm, and is deleted. A handle +// is minted by the CLIENT and arrives in the record; nothing on this side touches the client's +// slot allocator (the link ratchet's P13 exit gate pins that). namespace MobileGL::MG_Backend::DirectGLES { @@ -148,7 +116,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // True when this process runs the {slot, gen} arm. Fixed for the life of the process: the // two arms hold their twins in different containers, so flipping mid-run would strand them. // - // INLINE on purpose. Every Find / GetOrCreate / HandleOf / ForEachLive on the twin tables + // INLINE on purpose. Every FindByHandle / GetOrCreate / ForEachLive on the twin tables // consults it, i.e. it is on the per-draw path several times per draw. As an out-of-line // function in Managers.cpp (no LTO in any shipped configuration) that was a call through // the PLT per lookup; here the caller sees a guard-variable load and a perfectly-predicted @@ -181,7 +149,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // are two clients, and nothing may be shared with either from the other. // // THE HOLDER LIST STAYS PROCESS-WIDE AND THAT IS NOT A COMPROMISE. A notice is about an - // OBJECT, not about a bucket: OnFrontendObjectDestroyed and ReleaseTwinByHandle must reach + // OBJECT, not about a bucket: ReleaseTwinByHandle must reach // the by-value copies a fixture or a context reset is holding, whichever bucket they sit in, // and "which bucket holds it" is exactly the question that produced the two-holder leak the // list exists to fix. Each SHARED holder of the list is ONE bucket; the walk therefore covers @@ -328,15 +296,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return Shared(key)->GetOrCreate(handle); } Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); } - MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const { - return ForCallingThread().HandleOf(stateObj); - } - void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) { - ForCallingThread().NoteStateForHandle(handle, stateObj); - } - StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const { - return ForCallingThread().StateForHandle(handle); - } template void ForEachLive(Fn&& fn) const { // THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer @@ -361,9 +320,6 @@ namespace MobileGL::MG_Backend::DirectGLES { static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) { return Table::ReleaseTwinByHandle(handle); } - static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) { - return Table::OnFrontendObjectDestroyed(lifetimeId); - } // The instance spelling, and it HANDS THE TWIN OUT rather than answering a Bool: the two // tables that ARE a bucket registry (buffers, sampler views) have always been called as // `g_backendBufferResources.ReleaseByHandle(res)` and their callers still have to decide @@ -405,8 +361,6 @@ namespace MobileGL::MG_Backend::DirectGLES { template class BackendSlotTable { public: - using StatePtr = SharedPtr; - using StateWeakPtr = std::weak_ptr; using BackendPtr = SharedPtr; // The largest slot index this table will grow to for a handle that ARRIVED in a call's @@ -436,14 +390,6 @@ namespace MobileGL::MG_Backend::DirectGLES { struct Entry { BackendPtr backend; - // THE MONOLITH-GLUE HALF of the entry (P5e, CONTRACT-P5E §4.1): written by the - // minting GetOrCreate and by NoteStateForHandle, read only by StateForHandle and - // the legacy death notice. Never compared against another object to decide identity - // - that is what Gen is for - never dereferenced for its address, and never read to - // decide whether the slot is dead: death is announced, not discovered. ForEachLive - // stopped reading it at P5e: a walk that handed a frontend SharedPtr out of server - // memory on every step was the last place the server could hold one across records. - StateWeakPtr stateRef; // The generation this entry's twin was built for. An entry whose Gen no longer // matches the allocator's is a twin of the slot's PREVIOUS owner. Uint32 Gen = 0; @@ -453,27 +399,22 @@ namespace MobileGL::MG_Backend::DirectGLES { // Every constructor links the table into the per-type holder list and the destructor // unlinks it, so a by-value copy (the ScopedDirectGLESTextureBindings fixture's saved // registry) is a holder for exactly as long as it exists. Copy and move carry the - // ENTRIES and the memo; the links are the table's own and are never copied. + // ENTRIES; the links are the table's own and are never copied. BackendSlotTable() { LinkHolder(); } BackendSlotTable(const BackendSlotTable& other): m_ownerSession(other.m_ownerSession), m_slots(other.m_slots), m_band(other.m_band), - m_nullTwin(other.m_nullTwin), - m_memoLifetimeId(other.m_memoLifetimeId), - m_memoHandle(other.m_memoHandle) { + m_nullTwin(other.m_nullTwin) { LinkHolder(); } BackendSlotTable(BackendSlotTable&& other) noexcept: m_ownerSession(other.m_ownerSession), m_slots(std::move(other.m_slots)), m_band(std::move(other.m_band)), - m_nullTwin(std::move(other.m_nullTwin)), - m_memoLifetimeId(other.m_memoLifetimeId), - m_memoHandle(other.m_memoHandle) { + m_nullTwin(std::move(other.m_nullTwin)) { other.m_slots.clear(); other.m_band.clear(); - other.ForgetHandle(); LinkHolder(); } BackendSlotTable& operator=(const BackendSlotTable& other) { @@ -481,8 +422,6 @@ namespace MobileGL::MG_Backend::DirectGLES { m_slots = other.m_slots; m_band = other.m_band; m_nullTwin = other.m_nullTwin; - m_memoLifetimeId = other.m_memoLifetimeId; - m_memoHandle = other.m_memoHandle; } return *this; } @@ -491,11 +430,8 @@ namespace MobileGL::MG_Backend::DirectGLES { m_slots = std::move(other.m_slots); m_band = std::move(other.m_band); m_nullTwin = std::move(other.m_nullTwin); - m_memoLifetimeId = other.m_memoLifetimeId; - m_memoHandle = other.m_memoHandle; other.m_slots.clear(); other.m_band.clear(); - other.ForgetHandle(); } return *this; } @@ -507,88 +443,15 @@ namespace MobileGL::MG_Backend::DirectGLES { void SetOwnerSession(Uint64 session) { m_ownerSession = session; } Uint64 OwnerSession() const { return m_ownerSession; } - // Resolve-or-create. The handle comes from the client allocator keyed on the frontend - // object's lifetime id, so two calls for the same live object always land on the same - // slot, and a successor object at the same heap address never does. - BackendPtr& GetOrCreate(const StatePtr& stateObj) { - // No assert on null here, unlike the map arm: null is TOLERATED, so a DEBUG build - // must not trap where the release build quietly does the documented thing. - if (stateObj == nullptr) { - // The registry this replaces inserted a null KEY and handed back that entry's - // twin (DirectGLES.cpp's SyncTextureObjectToBackend documents relying on - // exactly that tolerance), so a release build never dereferenced null here. - // Keep the shape exactly, INCLUDING across calls: the map kept its null-keyed - // entry, so a second null call was handed the same twin the first one got. - // Resetting here instead would have destroyed it - an arm difference in the one - // path that documents relying on this. One per-table parking slot, never live, - // never handed a handle, because a null object has no identity and - // therefore cannot have a {slot, gen}. - return m_nullTwin; - } - - // D13: the teardown sentinel is armed by the slot table's first insertion. Twin - // creation is the moment a driver-owned id starts needing a guarded destructor; - // this is the cold path, so the once-guard costs nothing per draw. On the legacy - // arm StateBackendObjectRegistry::GetOrCreate arms it itself. - EnsureProcessTeardownSentinel(); - -#if MOBILEGL_BUILD_DISAGGREGATED - // This overload mints from frontend identity and is monolith-only. Refuse - // before either the client object or allocator is touched, even when a - // legacy named scope is open and the current record is barriered. - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("GetOrCreate(StatePtr)"); -#endif - - const MG_Pipe::MGPipeHandle handle = - MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId()); - MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle), - "MGPipe slot space of kind %u is exhausted", - static_cast(kKind)); - Entry& entry = EntryAt(handle.Slot); - if (entry.Live && entry.Gen != handle.Gen) { - // The slot was reclaimed and handed to a new object: the twin at it describes - // driver ids the new state object never made. - entry.backend.reset(); - } - entry.Gen = handle.Gen; - entry.Live = true; - entry.stateRef = stateObj; - // No creation tick and no sweep here. The registry this replaces needed both, - // because nothing told it a texture or a renderbuffer had been DELETED and object - // CHURN rather than draw count is what made that urgent. Every one of the six kinds - // now announces its own death from its destructor, so a dead twin's slot is already - // back before the next creation asks for one. - RememberHandle(stateObj->GetLifetimeId(), handle); - return entry.backend; - } - - // P3a: resolve-or-create BY HANDLE, and it is the shape that discharges the debt this - // header records against itself at the top of the file. + // P3a: resolve-or-create BY HANDLE - since P13 the only resolve-or-create there is. The + // handle ARRIVED, in the call's payload, already minted by the client; this never + // touches the allocator. It indexes the slot, notices a generation that no longer + // matches (the slot was recycled, so the twin at it describes driver ids the new + // resource never made) and hands back the twin pointer. // - // The overload above mints - it calls MGPipeSlots().Acquire off a frontend object's - // lifetime id, from inside MG_Backend - which is monolith glue: a handle is minted by - // the CLIENT, and under a real split neither the object nor its lifetime id exists on - // this side. This overload never touches the allocator at all. The handle ARRIVED, in - // the call's payload, already minted by the side that owns minting; all this does is - // index the slot, notice a generation that no longer matches (the slot was recycled, - // so the twin at it describes driver ids the new resource never made) and hand back - // the twin pointer. FindByHandle beside it is the same shape and already existed. + // Death stays ANNOUNCED: the family's own destroy call or the shared death notice + // releases the twin by handle, and the slot is freed by the CLIENT after that returns. // - // No StatePtr, therefore no Entry::stateRef unless a caller that holds the object notes - // it (NoteStateForHandle): a handle-keyed entry has no frontend object to weakly hold - // by itself. P5e (§4.1) makes that stop mattering for the iteration - ForEachLive keys - // on Live && backend, so such an entry is VISIBLE to the walk and it is StateForHandle, - // asked by the one caller that needs the object, that answers null for it. Which is the - // same set the old stateRef-locking walk produced, decided at one site instead of in - // the loop. - // - // Death stays ANNOUNCED, as it is on the other overload: for a handle-keyed kind the - // announcement is the family's own destroy call, not the shared death notice, and the - // slot is freed by the CLIENT after that call returns. - // - // UNUSED AT THE CONTRACT COMMIT, deliberately: it is a member of a class template, so - // an uninstantiated one costs nothing anywhere, and the backend package is what gives - // it its first caller. BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) { MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle), "GetOrCreate(handle) named the reserved null handle"); @@ -618,15 +481,11 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif } - // Same arming as the minting overload, and for the same reason: twin creation is - // the moment a driver-owned id starts needing a guarded destructor. + // Twin creation is the moment a driver-owned id starts needing a guarded destructor. EnsureProcessTeardownSentinel(); - // THE TWO DIRECTIONS ARE NOT SYMMETRIC HERE, where they are on the minting overload. - // There the handle comes straight out of MGPipeSlots().Acquire and can never be - // BEHIND the entry, so a bare `!=` only ever means "the slot was recycled forward". - // Here the handle arrived in a payload, so `handle.Gen < entry.Gen` is a reachable - // input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id, + // THE TWO DIRECTIONS ARE NOT SYMMETRIC. The handle arrived in a payload, so + // `handle.Gen < entry.Gen` is a reachable input, and adopting it would destroy the INCUMBENT LIVE twin - a driver buffer id, // a persistent map, a pooled store, released by a defaulted destructor that issues // no glDeleteBuffers and no pool enrolment - and then stamp the slot back to the // dead resource's generation, after which the incumbent's own FindByHandle refuses @@ -668,31 +527,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return entry->Live ? entry->Gen : 0; } - // Remember/read the frontend object last used by the monolith sync path. - // Handles supplied to the server never need this weak state. Neither method - // touches the allocator, so each carries its own unconditional apply guard. - void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("NoteStateForHandle"); -#endif - if (MG_Pipe::MGPipeHandleIsNull(handle)) return; - Entry* const entry = EntryOrNull(handle.Slot); - if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return; - entry->stateRef = stateObj; - } - - // The frontend object noted for this handle, or null. The handle answers identity; - // this answers only "which object was this twin last synced from". - StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const { -#if MOBILEGL_BUILD_DISAGGREGATED - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("StateForHandle"); -#endif - if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; - const Entry* const entry = EntryOrNull(handle.Slot); - if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return nullptr; - return entry->stateRef.lock(); - } - // P3a: the death half of the overload above, for a kind whose announcement is its own // destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather // than destroying it in place, because the caller may still have to decide what @@ -708,29 +542,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // left alone: the successor's own GetOrCreate resets it. BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) { if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{}; - if (m_memoHandle.Slot == handle.Slot) ForgetHandle(); Entry* const entry = EntryOrNull(handle.Slot); if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return BackendPtr{}; BackendPtr dead = std::move(entry->backend); entry->backend.reset(); - entry->stateRef.reset(); entry->Live = false; return dead; } - // Null when no live twin of this object exists. Unlike the registry's Find this NEVER - // mutates the table, so the returned pointer survives any later Find on it; only a - // GetOrCreate that grows the vector can move it, and callers that hold one across a - // possible insertion still copy the BackendPtr out. - BackendPtr* Find(StateObject* stateObj) { - if (stateObj == nullptr) return nullptr; - return FindByHandle(HandleOf(stateObj)); - } - - const BackendPtr* Find(StateObject* stateObj) const { - return const_cast(this)->Find(stateObj); - } - BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) { if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; Entry* const entry = EntryOrNull(handle.Slot); @@ -738,65 +557,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return &entry->backend; } - // The handle this object's twin is keyed on, or the null handle. This is what a backend - // memo stores instead of a raw pointer, a GL name or a bare lifetime id. - // - // A NULL answer is never memoised. The memo is per table and the allocator is per - // kind, so with two holders of one kind the OTHER table can be the one that acquires; - // a cached "no handle" here would then outlive the twin's creation over there, and - // nothing on this table's own acquire path would ever refresh it. A miss costs the - // allocator probe it always cost; a hit is refreshed the moment anyone acquires. - MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const { - if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle; -#if MOBILEGL_BUILD_DISAGGREGATED - // Resolve frontend identity only in the monolith path. A cache hit is - // subject to the same guard as a fresh allocator lookup. - MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread("HandleOf"); -#endif - const Uint64 lifetimeId = stateObj->GetLifetimeId(); - if (lifetimeId == m_memoLifetimeId) return m_memoHandle; - const MG_Pipe::MGPipeHandle handle = - MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId); - if (!MG_Pipe::MGPipeHandleIsNull(handle)) RememberHandle(lifetimeId, handle); - return handle; - } - - // P2 step e2's backend half. The frontend object with this lifetime id has just been - // DESTROYED: resolve its handle ONCE, drop its twin in EVERY table of this type, and - // return the slot to the allocator - in that order, because the allocator forgets the - // lifetime id on Free and a holder told second could no longer resolve it. - // - // The slot is returned whether or not any holder still had a twin at it: the lifetime - // id is dead and MG_State never hands one out twice, so nothing can acquire it again, - // and a slot minted for it that no table holds (a table reset with `= {}` drops its - // entries without freeing) would otherwise stay allocated for the life of the process. - // - // STATIC, and deliberately so: a notice is about an object, not about a table, and - // "which table holds it" is exactly the question that produced the two-holder leak. - // Returns whether the object had a slot of this kind, i.e. whether anything was freed; - // a second call for the same id answers false because the allocator no longer maps it. - static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) { -#if MOBILEGL_BUILD_DISAGGREGATED - // P5c (hd): the Find/Free pair below is monolith-only with an active transport - - // a frontend death is announced to the server by the object_death record (ct), - // and a direct call from the apply thread is Fatal{RoleViolation, "MGPipeSlots"}. - MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("OnFrontendObjectDestroyed"); -#endif - const MG_Pipe::MGPipeHandle handle = - MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, lifetimeId); - if (MG_Pipe::MGPipeHandleIsNull(handle)) return false; - for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) { - // The successor is read BEFORE the release: ReleaseTwinAt runs the twin's - // destructor, which is a driver call, and nothing that outlives it may be a - // reference into this holder. - BackendSlotTable* const next = holder->m_nextHolder; - holder->ReleaseTwinAt(handle); - holder = next; - } - MG_Pipe::MGPipeSlots().Free(kKind, handle); - return true; - } - // P5c (ct), CONTRACT-P5C.md §5.2: the handle-keyed half of the above, for a death that // arrived AS A WIRE RECORD (object_death) rather than as the shared notice. The // handle IS the resolution - it crossed in the record's payload - so the client's @@ -848,14 +608,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // it must not have - and replaces P3a's own fn(StatePtr, BackendPtr), which handed a // frontend SharedPtr OUT OF SERVER MEMORY on every step. // - // WHAT THE CALLER LOST AND HOW IT GETS IT BACK, because the two are not the same thing. - // The walk no longer locks Entry::stateRef, so a caller that needs the frontend object - // asks StateForHandle(handle) for it - which is the same weak reference, read at one - // named site inside the scope that owns the debt, instead of at every step of an - // iteration. Coverage is UNCHANGED by construction: an entry ForEachLive used to skip - // because its stateRef did not lock is one whose StateForHandle answers null, and the - // caller skips it there instead. - // // The handle is the entry's own {slot, gen}, band-aware: a composite ShaderCso's slot // is reported as kMGPipeShaderCsoCompositeSlotBase + its band index, i.e. the slot the // client minted, never the index into m_band. @@ -895,7 +647,6 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector slots = std::move(m_slots); Vector band = std::move(m_band); BackendPtr nullTwin = std::move(m_nullTwin); - ForgetHandle(); slots.clear(); band.clear(); nullTwin.reset(); @@ -931,13 +682,8 @@ namespace MobileGL::MG_Backend::DirectGLES { private: // Drop the twin at `handle` if THIS table holds it. Frees nothing: the slot belongs to - // the kind, not to the table, and OnFrontendObjectDestroyed returns it once, after - // every holder has let go. + // the kind, not to the table, and the client returns it once, after the release. Bool ReleaseTwinAt(MG_Pipe::MGPipeHandle handle) { - // Forget the memo whenever it names this slot, even if this table has no entry - // there: a memo can be a handle learned from the allocator for an object another - // holder twinned, and it must not survive the slot's next handout. - if (m_memoHandle.Slot == handle.Slot) ForgetHandle(); // The twin's destructor is a driver call and could, in principle, re-enter // GetOrCreate on this table and resize m_slots. So NOTHING that outlives the // destructor may be a reference into m_slots: the twin is moved out into a local, @@ -948,7 +694,6 @@ namespace MobileGL::MG_Backend::DirectGLES { if (entry == nullptr || !entry->Live || entry->Gen != handle.Gen) return false; dead = std::move(entry->backend); entry->backend.reset(); - entry->stateRef.reset(); entry->Live = false; } dead.reset(); @@ -983,8 +728,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return const_cast(this)->EntryOrNull(slot); } - // Grows the right space to hold `slot`. Every caller bounds `slot` first - the minting - // overload because the allocator produced it, the handle overload against + // Grows the right space to hold `slot`. Every caller bounds `slot` first - against // kMaxHandleSlot - because this is the one place a client-supplied number decides an // allocation size. A composite slot grows m_band by (slot - base) + 1, so the ordinary // table never learns the band exists. @@ -995,15 +739,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return table[index]; } - void RememberHandle(Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) const { - m_memoLifetimeId = lifetimeId; - m_memoHandle = handle; - } - void ForgetHandle() const { - m_memoLifetimeId = 0; - m_memoHandle = MG_Pipe::kMGPipeNullHandle; - } - // The holder list: intrusive and doubly linked, so registering and unregistering are // two pointer writes with no allocation, and its head is a constant-initialised // static - which is what lets the process-lifetime registry globals in Managers.cpp @@ -1041,30 +776,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // Handed back by GetOrCreate for a null state object. Never live, never handed a handle. BackendPtr m_nullTwin; - // ONE-entry resolution memo, lifetimeId -> handle. It exists because without it every - // resolution goes through the allocator's ByLifetimeId hash, which the deleted - // TwinLookupMemos existed to avoid and which D13 promises to replace with "direct slot - // indexing". - // - // It is one entry and therefore only helps a caller that asks for the SAME object twice - // running - ResolveVaoTwin and SyncCurrentProgram do, once per draw each. Two callers - // it does NOT help, recorded rather than claimed away: BindCurrentFBO resolves BOTH - // targets in a frame, and ResolveUnitSamplerBackend asks for a different sampler per - // texture unit, so both thrash a single-entry memo and pay the probe P1 did not (P1 had - // a per-unit memo and a direct-mapped 6-slot array there). Making the memo per-unit / - // per-target is the fix, and G11 - the device-side gate that would price it - is owed. - // - // It cannot serve a stale answer, by three independent arguments: - // * the key is a lifetime id, which MG_State never hands out twice, so a recycled - // heap address cannot hit this memo the way it could hit an address-keyed one; - // * a null answer is never stored, so another holder's acquire cannot be hidden by - // a "no handle" this table remembered earlier; and - // * even a hit for a slot that has since been freed and re-handed is caught, because - // the caller resolves the handle through FindByHandle, which compares Gen. - // Cleared anyway when a death notice names the memoised slot. 0 is never a live - // lifetime id (MG_State's counters start at 1), so a zeroed memo is a guaranteed miss. - mutable Uint64 m_memoLifetimeId = 0; - mutable MG_Pipe::MGPipeHandle m_memoHandle = MG_Pipe::kMGPipeNullHandle; }; } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 74813f91..de44de12 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -13,6 +13,7 @@ #include "MG_State/GLState/FramebufferState/FramebufferObject.h" #include "MG_State/GLState/Core.h" #include +#include #include "MG_State/GLState/TextureState/TextureState.h" #include "MG_Util/Classifiers/TextureEnumClassifier.h" #include "MG_Util/Converters/MGToGL/TextureEnumConverter.h" @@ -453,9 +454,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } UpdateDynamicBackendParameters(); UpdateAdvertisedExtensions(); - if (MG_Pipe::gPipeInputs.IsLive()) { - MG_Pipe::gPipeInputs.InvalidateCompileEnv(); - } + MG_Pipe::MGPipeNotifyCompileEnvChanged(); PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps, MutableFormatCapabilities()); PrintFormatCapabilities(GetFormatCapabilities()); @@ -1062,9 +1061,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_vulkanCaps = capabilities; UpdateDynamicBackendParameters(); UpdateAdvertisedExtensions(); - if (MG_Pipe::gPipeInputs.IsLive()) { - MG_Pipe::gPipeInputs.InvalidateCompileEnv(); - } + MG_Pipe::MGPipeNotifyCompileEnvChanged(); MutableFormatCapabilities().Clear(); } diff --git a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp index 597f9466..3f84835e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp @@ -87,7 +87,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { // What leaked was the SERVER's twin: a framebuffer has NO wire delete opcode at all // (BRIEF-P4A D-I2), so `object_death` is its only death delivery, and without a // consumer here the server's record stayed Live for the life of the session. - void OnMagmaFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { + void OnMagmaFrontendStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, + MG_Pipe::MGPipeHandle /*handle*/) { // Monolith has no record to emit and no server twin to tell. if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; // Raised ON the apply thread: the server role destroying a frontend object it diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 27e6fad9..d5aa03e7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -47,8 +47,6 @@ #include // P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on. #include -// P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope. -#include // P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops. #include "WireDeclineTally.h" #include "WireColorBlitFilter.h" @@ -4919,7 +4917,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 slot = freeSlot; if (slot == kNoSlot) { for (Uint32 candidate = 0; candidate < kNoSlot; ++candidate) { - if (MG_Pipe::gPipeInputs.HasOpenTransformFeedbackSpan(m_xfbCounterSlotOwner[candidate])) { + if (m_xfbCounterSlotOwner[candidate] != 0 && + MG_Pipe::MGPipeApplier().StreamOutputSpans.count(m_xfbCounterSlotOwner[candidate]) != 0) { continue; } if (slot == kNoSlot || m_xfbCounterSlotLastUse[candidate] < m_xfbCounterSlotLastUse[slot]) { diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 3bf63425..35450b36 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -1440,7 +1440,7 @@ namespace MobileGL::MG_Pipe { // are the same call on the same handle and the second is a no-op, because Free // bumps no generation (the bump rides the next handout) and refuses a slot that is // no longer live at this generation. - MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId); + MG_State::GLState::NotifyStateObjectDestroyed(MGPipeKind::VertexElementsCso, lifetimeId, handle); if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(MGPipeKind::VertexElementsCso, handle); return published; } @@ -1979,7 +1979,7 @@ namespace MobileGL::MG_Pipe { // P4a: whether a slot exists is this client's business, and a consumer that records // notices must not stop seeing a class announce itself. void NotifyAndFree(MGPipeKind kind, Uint64 lifetimeId, MGPipeHandle handle) { - MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId); + MG_State::GLState::NotifyStateObjectDestroyed(kind, lifetimeId, handle); if (!MGPipeHandleIsNull(handle)) MGPipeSlots().Free(kind, handle); } @@ -2265,6 +2265,17 @@ namespace MobileGL::MG_Pipe { return lifetimeId != 0 && MGPipeApplier().StreamOutputSpans.count(lifetimeId) != 0; } + namespace { + // The frontend half of MGPipeNotifyCompileEnvChanged (PipeApply.h): registered once, at + // static initialisation, by the translation unit that owns the forward. + const Bool kCompileEnvHookRegistered = [] { + MGPipeSetCompileEnvChangedHook([] { + if (gPipeInputs.IsLive()) gPipeInputs.InvalidateCompileEnv(); + }); + return true; + }(); + } + void PipeInputs::InvalidateCompileEnv() { MGP_STICKY_FORWARD_PULL(InvalidateCompileEnv); #if MOBILEGL_BUILD_DISAGGREGATED diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp index c8b97749..4ee5f313 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.cpp @@ -21,12 +21,6 @@ namespace MobileGL::MG_Pipe { #if MOBILEGL_BUILD_DISAGGREGATED - Bool MGPipeApplierIsUnbarrieredApply() { - if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return false; - if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return false; - return !MGPipeApplierCurrentRecordIsBarriered(); - } - void MGPipeRefuseAllocatorFromApplyThread(const char* entry) { if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; // D10: OR'd with the PROCESS-ROLE fact, not replaced by it. OnApplyThread() answers @@ -49,25 +43,6 @@ namespace MobileGL::MG_Pipe { std::abort(); } - // P5f (fr): all frontend-identity registry surfaces are monolith glue, including - // the ones that do not touch the allocator. Neither a wait nor a named scope can - // make a frontend SharedPtr exist in a separate server process. - void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry) { - if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; - // D10: OR'd with the PROCESS-ROLE fact, not replaced by it. OnApplyThread() answers - // about the INPROC applier thread and is the only fact a ServerLoop fixture with no - // client session has; MGPipeServerArm() adds the spawn case, where EVERY thread is a - // server thread. Replacing the first with the second disarmed the refusal in every unit - // test that drives the apply thread directly - measured, 14 RemoteGuards cases went - // green-by-omission before this was put back as a disjunction. - if (!MG_Remote::Server::ServerLoop::OnApplyThread() && !MG_Pipe::MGPipeServerArm()) return; - MGLOG_F("MGPipe: Fatal{RoleViolation, \"MGPipeSlots\"} - the apply thread reached " - "BackendSlotTable::%s. Frontend-identity registry operations are monolith-only, " - "including barriered records; resolve the twin from the record's handle instead", - entry); - std::abort(); - } - #endif namespace { diff --git a/MobileGL/MG_Impl/Pipe/SlotAllocator.h b/MobileGL/MG_Impl/Pipe/SlotAllocator.h index f4c82564..1224ed97 100644 --- a/MobileGL/MG_Impl/Pipe/SlotAllocator.h +++ b/MobileGL/MG_Impl/Pipe/SlotAllocator.h @@ -173,57 +173,8 @@ namespace MobileGL::MG_Pipe { // pull build's bytes do not move (G1). void MGPipeRefuseAllocatorFromApplyThread(const char* entry); - // Shared predicate for remaining field/verb guards. Registry and allocator - // guards below refuse both barriered and unbarriered transport apply. - Bool MGPipeApplierIsUnbarrieredApply(); - - // P5f (fr): frontend-object twin lookup, minting and weak-state access are refused - // on EVERY transport apply, including barriered records and both named scopes. - // The handle overloads remain server-local; the frontend overloads remain monolith - // glue. The registry guard is independent of the allocator guard. - void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char* entry); - - // P5f (fr): these historical scope names remain greppable markers around - // monolith glue until P3b/P4b remove those bodies. They hold no state and grant - // no exemption: allocator and frontend-registry access always refuse apply. - // - // P7 wave 2-B2: MagmaP7AllocatorDebtScope has NO BACKEND USES LEFT. P5e ruling 12 named - // four Magma apply-thread allocator debts (VulkanRenderer.cpp x3, ResourceTracker.h x1); - // P5f fv and P5f fm retired two of them, and this package retires the surviving pair - - // ShutdownBlitResources and ShutdownDepthMipmapResources, where the objects the debt was - // about are simply never created off the monolith arm. The class stays because - // MG_Test/Wire/RemoteClientTest uses it as a NEGATIVE CONTROL: two RemoteGuards cases - // construct it on the apply thread and assert the allocator refuses anyway, which is the - // property P5f (fr) established and the only thing this name now asserts. - class MagmaP7AllocatorDebtScope { - public: - MagmaP7AllocatorDebtScope() = default; - ~MagmaP7AllocatorDebtScope() = default; - MagmaP7AllocatorDebtScope(const MagmaP7AllocatorDebtScope&) = delete; - MagmaP7AllocatorDebtScope& operator=(const MagmaP7AllocatorDebtScope&) = delete; - }; - - class MGPipeFrontendKeyedRegistryScope { - public: - MGPipeFrontendKeyedRegistryScope() = default; - ~MGPipeFrontendKeyedRegistryScope() = default; - MGPipeFrontendKeyedRegistryScope(const MGPipeFrontendKeyedRegistryScope&) = delete; - MGPipeFrontendKeyedRegistryScope& operator=(const MGPipeFrontendKeyedRegistryScope&) = delete; - }; - #else - // P13 W5: no transport, no apply thread - the role guards have nothing to refuse, and the two - // scopes nothing to mark (the record arm calls all of them in every build). + // P13 W5: no transport, no apply thread - the allocator's role guard has nothing to refuse. inline void MGPipeRefuseAllocatorFromApplyThread(const char*) {} - inline Bool MGPipeApplierIsUnbarrieredApply() { return false; } - inline void MGPipeRefuseFrontendKeyedRegistryFromApplyThread(const char*) {} - class MagmaP7AllocatorDebtScope { - public: - MagmaP7AllocatorDebtScope() = default; - }; - class MGPipeFrontendKeyedRegistryScope { - public: - MGPipeFrontendKeyedRegistryScope() = default; - }; #endif } // namespace MobileGL::MG_Pipe diff --git a/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp index d5cc81e3..935e8686 100644 --- a/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/P4aSeamPeek.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #define MGITEST_P4A_SEAM_PEEK_LIVE 1 #endif @@ -89,7 +90,11 @@ namespace MGITest { const auto& object = MobileGL::MG_State::pGLContext->GetSamplerObject( static_cast(glSamplerName)); if (object) { - if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.Find(object.get()); slot && *slot) { + // Resolved the client's way (the harness is the client): the object's identity handle, + // if one was ever minted, then the twin at it. + const MGP::MGPipeHandle identity = + MGP::MGPipeSlots().FindByLifetimeId(MGP::MGPipeKind::SamplerCso, object->GetLifetimeId()); + if (auto* const slot = MGB::SamplerImpl::g_backendSamplerObjects.FindByHandle(identity); slot && *slot) { out->IdentityTwinSamplerId = static_cast((*slot)->GetBackendSamplerId()); } } diff --git a/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp b/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp index f4aa2a82..de8eae5d 100644 --- a/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/PipeApplyPeek.cpp @@ -16,9 +16,9 @@ #include #include #include +#include #if MOBILEGL_BUILD_DISAGGREGATED #include -#include #include #include #endif @@ -49,7 +49,11 @@ namespace MGITest { MGB::TextureImpl::BackendTextureObject* EsprytTwin(unsigned glTextureName) { MobileGL::MG_State::GLState::ITextureObject* const object = FrontendTexture(glTextureName); if (object == nullptr) return nullptr; - auto* const found = MGB::TextureImpl::g_backendTextureObjects.Find(object); + // The harness is the client: it resolves the handle in the client's allocator and asks + // Espryt's table by handle, as the record does (P13 retired the object-keyed lookup). + const MGP::MGPipeHandle handle = + MGP::MGPipeSlots().FindByLifetimeId(MGP::MGPipeKind::Texture, object->GetLifetimeId()); + auto* const found = MGB::TextureImpl::g_backendTextureObjects.FindByHandle(handle); if (found == nullptr || !*found) return nullptr; return found->get(); } @@ -229,12 +233,12 @@ namespace MGITest { // Espryt must be the backend running, or "no view" would be true of every texture on // every other backend and the assertion would be vacuous where it is loudest. if (EsprytTwin(glTextureName) == nullptr) return false; - // HandleOfSamplerViewForTexture is the monolith glue that derives the view's handle from - // the TEXTURE's lifetime id (D-F2: one view per ITextureObject), so this asks Espryt's - // own table the same way Espryt asks it - it does not consult the applier record's - // ViewCso, which is the client's statement about the same fact and would make one side - // of the seam vouch for the other. - const MGP::MGPipeHandle view = MGB::SamplerViewImpl::HandleOfSamplerViewForTexture(object); + // The view's handle is minted off the TEXTURE's lifetime id (D-F2: one view per + // ITextureObject); the harness resolves it in the client's allocator and asks Espryt's own + // table by it - it does not consult the applier record's ViewCso, which is the client's + // statement about the same fact and would make one side of the seam vouch for the other. + const MGP::MGPipeHandle view = + MGP::MGPipeSlots().FindByLifetimeId(MGP::MGPipeKind::SamplerViewCso, object->GetLifetimeId()); if (MGP::MGPipeHandleIsNull(view)) { *outExists = false; return true; diff --git a/MobileGL/MG_Pipe/PipeApply.cpp b/MobileGL/MG_Pipe/PipeApply.cpp index 787e0112..91d4c833 100644 --- a/MobileGL/MG_Pipe/PipeApply.cpp +++ b/MobileGL/MG_Pipe/PipeApply.cpp @@ -781,6 +781,18 @@ namespace MobileGL::MG_Pipe { return (appliers.fetch_add(1, std::memory_order_relaxed) + 1) << 40; } + namespace { + std::atomic g_compileEnvChangedHook{nullptr}; + } + + void MGPipeSetCompileEnvChangedHook(MGPipeCompileEnvChangedHook hook) { + g_compileEnvChangedHook.store(hook, std::memory_order_release); + } + + void MGPipeNotifyCompileEnvChanged() { + if (const auto hook = g_compileEnvChangedHook.load(std::memory_order_acquire)) hook(); + } + MGPipeApplierState& MGPipeApplier() { const MGPipeApplierKeyResolver resolve = g_applierKeyResolver; if (resolve == nullptr) return g_defaultApplier; diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 2903c825..04013810 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -1019,6 +1019,16 @@ namespace MobileGL::MG_Pipe { // is byte-for-byte the old behaviour. MGPipeApplierState& MGPipeApplier(); + // P13 W6: the backend's "my capabilities changed" notice, without naming the frontend. A + // backend whose advertised limits / extensions move after a context exists (Magma's caps + // re-query) calls MGPipeNotifyCompileEnvChanged(); the frontend registers the hook that + // invalidates its shader compile environment (PipeFill.cpp, static registration). Under a + // transport no hook is registered on the server side and the client invalidates on the caps + // re-publication instead (CONTRACT-P5C §4.2), so the notice is a no-op there. + using MGPipeCompileEnvChangedHook = void (*)(); + void MGPipeSetCompileEnvChangedHook(MGPipeCompileEnvChangedHook hook); + void MGPipeNotifyCompileEnvChanged(); + // ===================================================================================== // P14 S5 (docs/Disaggregated/design/11-state-ownership.md): WHOSE APPLIER IS IT? // ===================================================================================== diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 238197d7..740ad28e 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -1249,7 +1249,8 @@ namespace MobileGL::MG_Remote::Client { } namespace { - void WireStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { + void WireStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, + MG_Pipe::MGPipeHandle /*handle*/) { (void)EmitObjectDeathRecord(kind, lifetimeId); } diff --git a/MobileGL/MG_State/GLState/StateObjectDeathNotice.h b/MobileGL/MG_State/GLState/StateObjectDeathNotice.h index ab25ceb8..adfd2fb1 100644 --- a/MobileGL/MG_State/GLState/StateObjectDeathNotice.h +++ b/MobileGL/MG_State/GLState/StateObjectDeathNotice.h @@ -37,8 +37,11 @@ namespace MobileGL::MG_State::GLState { struct StateObjectDeathOps { // The last SharedPtr to the frontend object with this lifetime id has dropped. - // Called from the object's destructor, so the object must NOT be touched. - void (*OnDestroyed)(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) = nullptr; + // Called from the object's destructor, so the object must NOT be touched. `handle` is + // the object's {slot, gen} of this kind, resolved by the caller while it still + // resolves (null when the object never had one); P13 keys the backend's twin release + // on it, so no consumer probes the allocator by lifetime id. + void (*OnDestroyed)(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, MG_Pipe::MGPipeHandle handle) = nullptr; }; inline const StateObjectDeathOps* g_stateObjectDeathOps = nullptr; @@ -51,10 +54,11 @@ namespace MobileGL::MG_State::GLState { return g_stateObjectDeathOps; } - inline void NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { + inline void NotifyStateObjectDestroyed(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, + MG_Pipe::MGPipeHandle handle) { const StateObjectDeathOps* ops = g_stateObjectDeathOps; if (ops == nullptr || ops->OnDestroyed == nullptr) return; - ops->OnDestroyed(kind, lifetimeId); + ops->OnDestroyed(kind, lifetimeId, handle); } } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 8f59981d..cd1c3387 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -3321,39 +3321,45 @@ namespace { } // namespace +namespace { + // The CLIENT's half of every case below, played by the case itself: the handle a record would + // carry, minted in the client's allocator. Since P13 the table never mints (it has no entry that + // could); it only receives handles, exactly as here. + MobileGL::MG_Pipe::MGPipeHandle MintFor(MobileGL::MG_Pipe::MGPipeKind kind, MobileGL::Uint64 lifetimeId) { + return MobileGL::MG_Pipe::MGPipeSlots().Acquire(kind, lifetimeId); + } +} // namespace + // The property the whole slice exists for. The pre-P2 registry keyed twins on the frontend heap // ADDRESS and defended the recycle with a weak_ptr; here the key is {slot, gen}, so a successor -// object landing on a slot its predecessor owned is a DIFFERENT handle, and the predecessor's -// handle resolves to nothing rather than to the successor's twin. +// landing on a slot its predecessor owned is a DIFFERENT handle, it is handed an EMPTY twin, and +// the predecessor's handle resolves to nothing rather than to the successor's twin. TEST(DirectGLESSlotTable, ARecycledSlotIsANewHandleAndTheStaleOneResolvesToNothing) { using namespace MobileGL; + constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query; FakeSlotTable table; - auto first = MakeShared(0xA1u); - table.GetOrCreate(first) = MakeShared(); - (*table.Find(first.get()))->marker = 1; - - const MG_Pipe::MGPipeHandle firstHandle = table.HandleOf(first.get()); + const MG_Pipe::MGPipeHandle firstHandle = MintFor(kKind, 0xA1u); ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(firstHandle)); + table.GetOrCreate(firstHandle) = MakeShared(); + (*table.FindByHandle(firstHandle))->marker = 1; EXPECT_EQ(table.LiveCount(), 1u); - // The frontend object dies and announces it (FakeStateObject is not one of the six re-keyed - // classes, so the notice its destructor would raise is raised by hand), and the slot is - // reclaimed - which is the only moment Gen moves. - first.reset(); - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xA1u)); + // The object dies: the twin is released by its handle (the death notice's release), then the + // client returns the slot - which is the only moment Gen can move, on the next handout. + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(firstHandle)); + MG_Pipe::MGPipeSlots().Free(kKind, firstHandle); EXPECT_EQ(table.LiveCount(), 0u); EXPECT_EQ(table.FindByHandle(firstHandle), nullptr) << "a handle whose object is gone still resolved to a twin"; - auto second = MakeShared(0xA2u); - table.GetOrCreate(second) = MakeShared(); - (*table.Find(second.get()))->marker = 2; - - const MG_Pipe::MGPipeHandle secondHandle = table.HandleOf(second.get()); + const MG_Pipe::MGPipeHandle secondHandle = MintFor(kKind, 0xA2u); EXPECT_EQ(secondHandle.Slot, firstHandle.Slot) << "the free list did not hand the slot back"; EXPECT_NE(secondHandle.Gen, firstHandle.Gen) << "the generation did not move on slot reuse"; - EXPECT_FALSE(firstHandle == secondHandle); + EXPECT_EQ(table.GetOrCreate(secondHandle), nullptr) + << "the successor at a recycled slot inherited its predecessor's twin"; + table.GetOrCreate(secondHandle) = MakeShared(); + (*table.FindByHandle(secondHandle))->marker = 2; // The stale handle must not resolve to its successor's twin. This is the ABA the address key // could only paper over. @@ -3361,113 +3367,82 @@ TEST(DirectGLESSlotTable, ARecycledSlotIsANewHandleAndTheStaleOneResolvesToNothi ASSERT_NE(table.FindByHandle(secondHandle), nullptr); EXPECT_EQ((*table.FindByHandle(secondHandle))->marker, 2); - second.reset(); - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xA2u)); + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(secondHandle)); + MG_Pipe::MGPipeSlots().Free(kKind, secondHandle); } -// Gen moves on reuse and ONLY on reuse: a live object that is looked up again, or respecified, -// keeps the handle it was minted with (MGPipeHandles.h). +// Gen moves on reuse and ONLY on reuse: a live entry that is resolved again - by GetOrCreate or by +// FindByHandle, any number of times - keeps the one twin it was built with (MGPipeHandles.h). TEST(DirectGLESSlotTable, RepeatedLookupsOfALiveObjectKeepOneHandle) { using namespace MobileGL; + constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query; FakeSlotTable table; - auto object = MakeShared(0xB1u); - // An object that is bound but never synced has no twin and no handle; asking is a miss. - EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(table.HandleOf(object.get()))); - table.GetOrCreate(object) = MakeShared(); - const MG_Pipe::MGPipeHandle handle = table.HandleOf(object.get()); - ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle)) - << "the memo went on answering a null handle after the twin was created"; + const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xB1u); + // A handle no record has resolved yet has no twin; asking is a miss, not a mint. + EXPECT_EQ(table.FindByHandle(handle), nullptr); + EXPECT_EQ(table.LiveCount(), 0u); + table.GetOrCreate(handle) = MakeShared(); + const FakeBackendObject* const twin = table.FindByHandle(handle)->get(); for (int i = 0; i < 8; ++i) { - auto* slot = table.GetOrCreate(object) ? table.Find(object.get()) : nullptr; - ASSERT_NE(slot, nullptr); - EXPECT_TRUE(table.HandleOf(object.get()) == handle) << "handle moved on lookup " << i; + EXPECT_EQ(table.GetOrCreate(handle).get(), twin) << "the twin was re-made on lookup " << i; + ASSERT_NE(table.FindByHandle(handle), nullptr); + EXPECT_EQ(table.FindByHandle(handle)->get(), twin); } EXPECT_EQ(table.LiveCount(), 1u); - object.reset(); - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xB1u)); + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle)); + MG_Pipe::MGPipeSlots().Free(kKind, handle); } -// The round-4 review's minor 8: HandleOf used to memoise a NULL answer, on the argument that -// GetOrCreate refreshes the memo. That holds for ONE table. The allocator is per kind and the -// memo is per table, so once a second holder of the kind can be the one that acquires, the first -// table's cached "no handle" outlives the twin's creation and nothing on its own acquire path -// ever corrects it - every Find through it is a miss for a twin that exists. This is that -// configuration, and it must resolve. -TEST(DirectGLESSlotTable, ANegativeLookupIsNotCachedAcrossAnotherHoldersAcquire) { - using namespace MobileGL; - - FakeSharedKindSlotTable first; - FakeSharedKindSlotTable second; - auto object = MakeShared(0xB2u); - - // `first` asks before anyone has acquired: a miss, which must NOT be remembered. - EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(first.HandleOf(object.get()))); - - // `second` is the holder that acquires. - second.GetOrCreate(object) = MakeShared(); - const MG_Pipe::MGPipeHandle handle = second.HandleOf(object.get()); - ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle)); - - // `first` never acquired, so nothing on its own path refreshed its memo; it must still - // answer the handle the kind now has for this object. - EXPECT_TRUE(first.HandleOf(object.get()) == handle) - << "the first holder kept answering the null handle it cached before the second holder " - "acquired, so every lookup through it misses a twin that exists"; - - object.reset(); - EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(0xB2u)); -} - -// The lookup does not mutate the table, which is what lets SyncTextureObjectToBackend stop paying -// a by-value copy plus a second Find to survive the registry's erase-inside-Find. An entry whose -// object has gone stays put until the death notice arrives, and a live entry's pointer is -// unaffected by looking up anything else. +// The lookup does not mutate the table: a returned pointer is an array element, so it survives any +// later lookup - including one for a handle whose twin is gone - and only a release takes an entry. TEST(DirectGLESSlotTable, FindNeverMutatesTheTable) { using namespace MobileGL; + constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query; FakeSlotTable table; - auto kept = MakeShared(0xC1u); - auto doomed = MakeShared(0xC2u); + const MG_Pipe::MGPipeHandle kept = MintFor(kKind, 0xC1u); + const MG_Pipe::MGPipeHandle doomed = MintFor(kKind, 0xC2u); table.GetOrCreate(kept) = MakeShared(); table.GetOrCreate(doomed) = MakeShared(); - auto* keptSlot = table.Find(kept.get()); + auto* keptSlot = table.FindByHandle(kept); ASSERT_NE(keptSlot, nullptr); const FakeBackendObject* keptTwin = keptSlot->get(); - // The object goes but its notice is deliberately withheld for a moment, so that whatever - // changes between here and the notice is Find's doing. The registry's Find would have - // erased the expired entry here and relocated the rest of the probe cluster, invalidating - // keptSlot. This one answers what it answers and touches nothing. - doomed.reset(); - EXPECT_EQ(table.Find(kept.get()), keptSlot); - EXPECT_EQ(table.LiveCount(), 2u) << "Find reclaimed a slot; only a death notice may do that"; + // Lookups of the other entry, of a stale generation of it and of the null handle change + // nothing. + (void)table.FindByHandle(doomed); + (void)table.FindByHandle(MG_Pipe::MGPipeHandle{doomed.Slot, doomed.Gen + 1u}); + (void)table.FindByHandle(MG_Pipe::kMGPipeNullHandle); + EXPECT_EQ(table.FindByHandle(kept), keptSlot); + EXPECT_EQ(table.LiveCount(), 2u) << "a lookup reclaimed a slot; only a release may do that"; EXPECT_EQ(keptSlot->get(), keptTwin); - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xC2u)); + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(doomed)); + MG_Pipe::MGPipeSlots().Free(kKind, doomed); EXPECT_EQ(table.LiveCount(), 1u); - EXPECT_EQ(table.Find(kept.get())->get(), keptTwin); + EXPECT_EQ(table.FindByHandle(kept)->get(), keptTwin); - kept.reset(); - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xC1u)); + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(kept)); + MG_Pipe::MGPipeSlots().Free(kKind, kept); } // ScopedDirectGLESTextureBindings saves a whole twin table by value, resets it with `= {}` and // restores it. The slot table has to keep that shape or the fixture stops isolating anything - -// and the saved copy is a HOLDER for as long as it exists, so a death announced while it is -// held reaches it too (the round-4 review's minor 2, in the fixture's own shape). +// and the saved copy is a HOLDER for as long as it exists, so a release while it is held reaches +// it too (the round-4 review's minor 2, in the fixture's own shape). TEST(DirectGLESSlotTable, AWholeTableSavesResetsAndRestores) { using namespace MobileGL; + constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query; const Uint32 holdersBefore = FakeSlotTable::HolderCount(); FakeSlotTable table; - auto object = MakeShared(0xD1u); - table.GetOrCreate(object) = MakeShared(); - (*table.Find(object.get()))->marker = 7; - const MG_Pipe::MGPipeHandle handle = table.HandleOf(object.get()); + const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xD1u); + table.GetOrCreate(handle) = MakeShared(); + (*table.FindByHandle(handle))->marker = 7; const FakeSlotTable saved = table; EXPECT_EQ(FakeSlotTable::HolderCount(), holdersBefore + 2u) @@ -3476,79 +3451,59 @@ TEST(DirectGLESSlotTable, AWholeTableSavesResetsAndRestores) { EXPECT_EQ(FakeSlotTable::HolderCount(), holdersBefore + 2u) << "the reset changed the holder count - a temporary's registration leaked or the " "table's own was lost"; - EXPECT_EQ(table.Find(object.get()), nullptr) << "the reset left the twin reachable"; + EXPECT_EQ(table.FindByHandle(handle), nullptr) << "the reset left the twin reachable"; table = saved; - ASSERT_NE(table.Find(object.get()), nullptr); - EXPECT_EQ((*table.Find(object.get()))->marker, 7); + ASSERT_NE(table.FindByHandle(handle), nullptr); + EXPECT_EQ((*table.FindByHandle(handle))->marker, 7); // The object dies while BOTH the working table and the saved copy hold its twin. One - // notice, and neither may keep a live entry. - object.reset(); - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0xD1u)); + // release, and neither may keep a live entry. + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle)); + MG_Pipe::MGPipeSlots().Free(kKind, handle); EXPECT_EQ(table.FindByHandle(handle), nullptr); EXPECT_EQ(table.LiveCount(), 0u); EXPECT_EQ(saved.LiveCount(), 0u) - << "the saved copy kept the dead object's twin - the notice reached one holder only"; + << "the saved copy kept the dead object's twin - the release reached one holder only"; } -// The whole point of routing every twin through the client allocator: a table that keeps its own -// dense array still shares ONE identity per frontend object with every other holder of it. -// -// Comparing a.HandleOf(o) with b.HandleOf(o) alone would prove nothing - HandleOf never reads the -// table, so any two tables agree for any implementation. What is actually load-bearing, and what -// is asserted here, is that ONE slot of the kind is consumed for the object no matter how many -// tables hold a twin of it (a per-table allocator would pass the pure compare and fail this), and -// that the shared handle still addresses each table's OWN twin. +// One handle, two holders of the kind: the shared handle addresses each table's OWN twin, and +// neither table's twin is the other's. TEST(DirectGLESSlotTable, TwoTablesOfTheSameKindShareOneSlotAndKeepTheirOwnTwin) { using namespace MobileGL; - constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Fence; auto& slots = MG_Pipe::MGPipeSlots(); const Uint32 liveBefore = slots.LiveCount(kKind); FakeSharedKindSlotTable a; FakeSharedKindSlotTable b; - auto object = MakeShared(0xE1u); - a.GetOrCreate(object) = MakeShared(); - (*a.Find(object.get()))->marker = 1; - b.GetOrCreate(object) = MakeShared(); - (*b.Find(object.get()))->marker = 2; + const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xE1u); + ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle)); + a.GetOrCreate(handle) = MakeShared(); + (*a.FindByHandle(handle))->marker = 1; + b.GetOrCreate(handle) = MakeShared(); + (*b.FindByHandle(handle))->marker = 2; EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 1u) - << "the two tables minted a slot each; Magma's table would then resolve a different " - "handle for the same object than Espryt's"; - - const MG_Pipe::MGPipeHandle handle = a.HandleOf(object.get()); - ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle)); - EXPECT_TRUE(b.HandleOf(object.get()) == handle); - + << "twinning one handle in two tables consumed more than the client's one slot"; ASSERT_NE(a.FindByHandle(handle), nullptr); ASSERT_NE(b.FindByHandle(handle), nullptr); EXPECT_EQ((*a.FindByHandle(handle))->marker, 1); EXPECT_EQ((*b.FindByHandle(handle))->marker, 2); - // TWO HOLDERS OF ONE SLOT is a real configuration, not a test artefact (the - // ScopedDirectGLESTextureBindings fixture above holds a second live table of kind Texture - // for the length of a test), and the death of the object is what makes it sharp: the - // allocator forgets the lifetime id on Free, so a notice delivered to ONE holder leaves - // the other with a live entry - and its twin's driver storage - that nothing can resolve - // and nothing sweeps. OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind below is the case - // for that; here the cleanup only has to leave the kind's LiveCount where it was. - object.reset(); - EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(0xE1u)); - EXPECT_EQ(slots.LiveCount(kKind), liveBefore) - << "the shared slot outlived both holders and the object"; + // TWO HOLDERS OF ONE SLOT is a real configuration (the ScopedDirectGLESTextureBindings fixture + // holds a second live table of kind Texture for the length of a test); + // OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind below is the case for its death. + EXPECT_TRUE(FakeSharedKindSlotTable::ReleaseTwinByHandle(handle)); + slots.Free(kKind, handle); + EXPECT_EQ(slots.LiveCount(kKind), liveBefore) << "the shared slot outlived both holders"; } -// The round-4 review's minor 2, closed: one notice, EVERY holder. Before this the dispatcher -// told one table per kind and DestroyByLifetimeId freed only a slot THIS table held, so with -// the sweep retired the second holder kept a live entry, and the twin, for the life of the -// process. Three holders here - two independent tables and a by-value copy, which is exactly -// what the fixture makes - and a fourth that never twinned the object and must be untouched. +// The round-4 review's minor 2, closed: one release, EVERY holder. Three holders here - two +// independent tables and a by-value copy, which is exactly what the fixture makes - and a fourth +// that never twinned the handle and must be untouched. TEST(DirectGLESSlotTable, OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind) { using namespace MobileGL; - constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Fence; auto& slots = MG_Pipe::MGPipeSlots(); const Uint32 liveBefore = slots.LiveCount(kKind); @@ -3557,24 +3512,21 @@ TEST(DirectGLESSlotTable, OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind) { FakeSharedKindSlotTable a; FakeSharedKindSlotTable b; FakeSharedKindSlotTable bystander; - auto object = MakeShared(0xE2u); - auto uninvolved = MakeShared(0xE3u); + const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xE2u); + const MG_Pipe::MGPipeHandle uninvolved = MintFor(kKind, 0xE3u); - a.GetOrCreate(object) = MakeShared(); - b.GetOrCreate(object) = MakeShared(); + a.GetOrCreate(handle) = MakeShared(); + b.GetOrCreate(handle) = MakeShared(); bystander.GetOrCreate(uninvolved) = MakeShared(); const FakeSharedKindSlotTable copyOfA = a; // the fixture's saved registry EXPECT_EQ(FakeSharedKindSlotTable::HolderCount(), holdersBefore + 4u); - const MG_Pipe::MGPipeHandle handle = a.HandleOf(object.get()); - ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle)); // Observers on the twins, so "dropped" means destroyed and not merely unreachable. const std::weak_ptr twinA = *a.FindByHandle(handle); const std::weak_ptr twinB = *b.FindByHandle(handle); - EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 2u); - // ONE notice. The object is still alive so that nothing but the notice can be at work. - EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId())); + // ONE release. + EXPECT_TRUE(FakeSharedKindSlotTable::ReleaseTwinByHandle(handle)); EXPECT_EQ(a.FindByHandle(handle), nullptr) << "holder a kept the twin"; EXPECT_EQ(b.FindByHandle(handle), nullptr) << "holder b kept the twin"; @@ -3583,135 +3535,88 @@ TEST(DirectGLESSlotTable, OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind) { EXPECT_EQ(b.LiveCount(), 0u); EXPECT_TRUE(twinA.expired()) << "a's twin is unreachable but still allocated"; EXPECT_TRUE(twinB.expired()) << "b's twin is unreachable but still allocated"; - EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 1u) << "the slot was not returned exactly once"; - EXPECT_FALSE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId())) - << "a second notice for the same object found a slot to free"; + EXPECT_FALSE(FakeSharedKindSlotTable::ReleaseTwinByHandle(handle)) + << "a second release for the same handle found a twin to drop"; - // The holder that never twinned the object is exactly as it was. + // The holder that never twinned the handle is exactly as it was. EXPECT_EQ(bystander.LiveCount(), 1u); - ASSERT_NE(bystander.Find(uninvolved.get()), nullptr); + ASSERT_NE(bystander.FindByHandle(uninvolved), nullptr); - object.reset(); - uninvolved.reset(); - EXPECT_TRUE(FakeSharedKindSlotTable::OnFrontendObjectDestroyed(0xE3u)); + slots.Free(kKind, handle); + EXPECT_TRUE(FakeSharedKindSlotTable::ReleaseTwinByHandle(uninvolved)); + slots.Free(kKind, uninvolved); EXPECT_EQ(bystander.LiveCount(), 0u); EXPECT_EQ(slots.LiveCount(kKind), liveBefore); } -// The sweep and both of its drivers are RETIRED on this arm (ROADMAP.md:18's "delete the GC"), -// and this is the property that replaces them. The registry this table replaces learned of a -// death only by finding an expired weak_ptr, so it needed a 1024-call draw tick AND a -// 64-creation tick and still held up to 64 dead, gigabyte-sized twins at once. An announced -// death returns the slot before the next creation asks for one, so NOTHING accumulates - -// nothing below calls CollectGarbageIfNeeded() or CollectGarbageNow(), and on this arm the -// former does nothing at all. +// The sweep and both of its drivers are RETIRED (ROADMAP.md:18's "delete the GC"), and this is +// the property that replaces them: an announced death releases the twin before the next creation +// asks for a slot, so NOTHING accumulates under object churn. TEST(DirectGLESSlotTable, AnnouncedDeathKeepsObjectChurnFromAccumulatingWithoutASweep) { using namespace MobileGL; + constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query; auto& slots = MG_Pipe::MGPipeSlots(); constexpr Uint32 kChurn = 256u; - const Uint32 highWaterBefore = slots.HighWater(MG_Pipe::MGPipeKind::Query); - const Uint32 liveBefore = slots.LiveCount(MG_Pipe::MGPipeKind::Query); + const Uint32 highWaterBefore = slots.HighWater(kKind); + const Uint32 liveBefore = slots.LiveCount(kKind); FakeSlotTable table; Uint32 peakLive = 0; for (Uint32 i = 0; i < kChurn; ++i) { - auto object = MakeShared(0xF0000000ull + i); - table.GetOrCreate(object) = MakeShared(); + const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0xF0000000ull + i); + table.GetOrCreate(handle) = MakeShared(); peakLive = std::max(peakLive, table.LiveCount()); - // What a real object's destructor raises. FakeStateObject is not one of the six - // re-keyed frontend classes, so the firing half is driven by hand here; that those six - // classes really do fire it is EveryReKeyedObjectClassAnnouncesItsOwnDeath below, and - // that the registries answer it per kind is - // EverySwitchedOverKindResolvesItsTwinThroughTheHandleArm. - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId())); + // What a real object's death delivers (Espryt's notice consumer releases by the handle + // the notice carries; the client frees after it). That the six real classes do raise it + // is EveryReKeyedObjectClassAnnouncesItsOwnDeath below. + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle)); + slots.Free(kKind, handle); } EXPECT_EQ(peakLive, 1u) << peakLive << " twins were live at once with " << kChurn - << " objects churned and every death announced - the notice stopped freeing the twin"; + << " objects churned and every death announced - the release stopped dropping the twin"; EXPECT_EQ(table.LiveCount(), 0u); - EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore) - << "the churn leaked slots the announced deaths should have returned"; + EXPECT_EQ(slots.LiveCount(kKind), liveBefore) << "the churn leaked slots"; // 2 and not 1: on a cold allocator the high-water mark counts the RESERVED slot 0 - // (kMGPipeFirstAllocatableSlot is 1) as well as the one slot this loop recycles, and ctest - // runs every case in its own process, so this case sees a cold allocator. What the bound - // rules out is the thing that matters - 256 churned objects growing the space by 256. - EXPECT_LE(slots.HighWater(MG_Pipe::MGPipeKind::Query) - highWaterBefore, 2u) + // (kMGPipeFirstAllocatableSlot is 1) as well as the one slot this loop recycles. + EXPECT_LE(slots.HighWater(kKind) - highWaterBefore, 2u) << "the slot space grew with the churn instead of being recycled"; } -// The map arm inserted a null key and handed back that entry's twin, and -// SyncTextureObjectToBackend documents relying on it. Release builds compile the assert out, so -// on the handle arm this has to be a defined answer rather than a dereference of null. -TEST(DirectGLESSlotTable, GetOrCreateToleratesANullStateObject) { - using namespace MobileGL; - - FakeSlotTable table; - const SharedPtr none; - auto& twin = table.GetOrCreate(none); - EXPECT_EQ(twin, nullptr); - EXPECT_EQ(table.LiveCount(), 0u) << "a null object took a slot"; - EXPECT_EQ(table.Find(nullptr), nullptr); - EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(table.HandleOf(nullptr))); - - // ...and a SECOND null call is handed the same parking slot rather than destroying what the - // first one was given. The map arm kept its null-keyed entry until a sweep, so a table that - // reset here would answer differently on the two arms in the one path that documents - // relying on this tolerance. - twin = MakeShared(); - twin->marker = 5; - auto& again = table.GetOrCreate(none); - ASSERT_NE(again, nullptr) << "the second null call destroyed the first one's parked twin"; - EXPECT_EQ(again->marker, 5); - EXPECT_EQ(&again, &twin); - EXPECT_EQ(table.LiveCount(), 0u); -} -// P2 step e2, the backend half. A sweep is a stand-in for a death notice; this is the notice. -// Nothing below calls CollectGarbage*: the slot comes back, and the twin goes, at the moment -// the frontend says the object is gone - which for a texture atlas or a renderbuffer is the -// difference between freeing a driver allocation now and freeing it 64 creations from now. +// P2 step e2, the backend half. A sweep is a stand-in for a death notice; this is the release the +// notice drives. It is idempotent, and it is GENERATION-EXACT: a late release for a dead handle +// must not drop the twin of the slot's next owner. TEST(DirectGLESSlotTable, AnAnnouncedDeathReturnsTheSlotWithoutASweep) { using namespace MobileGL; + constexpr MG_Pipe::MGPipeKind kKind = MG_Pipe::MGPipeKind::Query; auto& slots = MG_Pipe::MGPipeSlots(); - const Uint32 liveBefore = slots.LiveCount(MG_Pipe::MGPipeKind::Query); + const Uint32 liveBefore = slots.LiveCount(kKind); FakeSlotTable table; - auto object = MakeShared(0x1E2A0001ull); - table.GetOrCreate(object) = MakeShared(); - const MG_Pipe::MGPipeHandle handle = table.HandleOf(object.get()); - ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle)); - EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore + 1u); + const MG_Pipe::MGPipeHandle handle = MintFor(kKind, 0x1E2A0001ull); + table.GetOrCreate(handle) = MakeShared(); + EXPECT_EQ(slots.LiveCount(kKind), liveBefore + 1u); - // The object is STILL ALIVE here, which is the point: there is no weak_ptr test anywhere - // that could fire, so anything that changes is the notice's doing and nothing else's. - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId())); - EXPECT_EQ(table.LiveCount(), 0u) << "the twin survived its own destroy notice"; + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(handle)); + EXPECT_EQ(table.LiveCount(), 0u) << "the twin survived its own release"; EXPECT_EQ(table.FindByHandle(handle), nullptr); - EXPECT_EQ(table.Find(object.get()), nullptr); - EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore) - << "the slot was not returned to the allocator"; + // Idempotent: a repeated release finds nothing and says so. + EXPECT_FALSE(FakeSlotTable::ReleaseTwinByHandle(handle)); + slots.Free(kKind, handle); + EXPECT_EQ(slots.LiveCount(kKind), liveBefore) << "the slot was not returned to the allocator"; - // Idempotent: the allocator no longer maps the id, so a repeated notice frees nothing and - // says so - and it says so for every holder, since the notice is about the object and not - // about a table. - EXPECT_FALSE(FakeSlotTable::OnFrontendObjectDestroyed(object->GetLifetimeId())); - - // A slot minted for an object that NO table holds any more - the fixture's `table = {}` - // drops entries without freeing - still goes back when the object dies: the id is dead and - // cannot be acquired again, so keeping the slot would be the process-lifetime leak the - // review named. - auto orphaned = MakeShared(0x1E2A0002ull); - { - FakeSlotTable transient; - transient.GetOrCreate(orphaned) = MakeShared(); - } - EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore + 1u); - orphaned.reset(); - EXPECT_TRUE(FakeSlotTable::OnFrontendObjectDestroyed(0x1E2A0002ull)) - << "a slot no holder had an entry for was left allocated"; - EXPECT_EQ(slots.LiveCount(MG_Pipe::MGPipeKind::Query), liveBefore); + // The successor at the recycled slot, and then a LATE release naming the dead generation. + const MG_Pipe::MGPipeHandle successor = MintFor(kKind, 0x1E2A0002ull); + ASSERT_EQ(successor.Slot, handle.Slot); + table.GetOrCreate(successor) = MakeShared(); + EXPECT_FALSE(FakeSlotTable::ReleaseTwinByHandle(handle)) + << "a release for a dead generation dropped the slot's next owner's twin"; + EXPECT_NE(table.FindByHandle(successor), nullptr); + EXPECT_TRUE(FakeSlotTable::ReleaseTwinByHandle(successor)); + slots.Free(kKind, successor); } // The firing side of e2, on ALL SIX re-keyed object classes - the round-3 review's MAJOR 3. @@ -3726,7 +3631,7 @@ TEST(DirectGLESSlotTable, EveryReKeyedObjectClassAnnouncesItsOwnDeath) { static Vector> notices; notices.clear(); const MG_State::GLState::StateObjectDeathOps recording = { - .OnDestroyed = [](MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { + .OnDestroyed = [](MG_Pipe::MGPipeKind kind, Uint64 lifetimeId, MG_Pipe::MGPipeHandle) { notices.emplace_back(kind, lifetimeId); }, }; @@ -3837,48 +3742,50 @@ namespace { // and that a successor object landing on the freed slot gets a different Gen while the // predecessor's handle resolves to nothing. template - void ExpectTheHandleArmDrivesThisKind(const char* kindName, Registry& registry, MakeObject make) { + void ExpectTheHandleArmDrivesThisKind(const char* kindName, MobileGL::MG_Pipe::MGPipeKind kind, + Registry& registry, MakeObject make) { using namespace MobileGL; auto first = make(); ASSERT_NE(first, nullptr) << kindName; - auto& firstTwin = registry.GetOrCreate(first); - ASSERT_NE(MG_State::GLState::GetStateObjectDeathOps(), nullptr) - << kindName << ": twinning an object did not install the death-notice consumer"; - - const MG_Pipe::MGPipeHandle firstHandle = registry.HandleOf(first.get()); - ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(firstHandle)) - << kindName << ": GetOrCreate on the real registry minted no handle, so this kind is " - "not running on the {slot, gen} arm at all"; - EXPECT_EQ(registry.Find(first.get()), &firstTwin) - << kindName << ": Find resolved a different twin slot than GetOrCreate handed back"; - EXPECT_EQ(registry.FindByHandle(firstHandle), &firstTwin) - << kindName << ": the handle does not address the twin GetOrCreate handed back"; - - // The frontend object dies. NOTHING below sweeps - the destructor's own notice is the - // only thing that can free the slot, which is what makes this the e2/e3 pair end to end. + // The client's mint (the record would carry this handle), then the backend's adoption. const Uint64 firstLifetimeId = first->GetLifetimeId(); + const MG_Pipe::MGPipeHandle firstHandle = MG_Pipe::MGPipeSlots().Acquire(kind, firstLifetimeId); + ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(firstHandle)) << kindName; + auto* const firstTwin = registry.GetOrCreateByHandle(firstHandle); + ASSERT_NE(firstTwin, nullptr) + << kindName << ": the real registry refused a handle the client minted, so this kind is " + "not running on the {slot, gen} arm at all"; + ASSERT_NE(MG_State::GLState::GetStateObjectDeathOps(), nullptr) + << kindName << ": no death-notice consumer is installed"; + EXPECT_EQ(registry.FindByHandle(firstHandle), firstTwin) + << kindName << ": the handle does not address the twin GetOrCreateByHandle handed back"; + + // The frontend object dies. NOTHING below sweeps - the destructor's own notice (which + // carries the handle) releases the twin and the client frees the slot after it, which is + // what makes this the e2/e3 pair end to end. first.reset(); EXPECT_EQ(registry.FindByHandle(firstHandle), nullptr) << kindName << ": the twin outlived the announced death of its object"; - EXPECT_FALSE(registry.DestroyByLifetimeId(firstLifetimeId)) + EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(MG_Pipe::MGPipeSlots().FindByLifetimeId(kind, firstLifetimeId))) << kindName << ": the slot was still held after its object announced its death"; auto second = make(); ASSERT_NE(second, nullptr) << kindName; - auto& secondTwin = registry.GetOrCreate(second); - const MG_Pipe::MGPipeHandle secondHandle = registry.HandleOf(second.get()); + const MG_Pipe::MGPipeHandle secondHandle = + MG_Pipe::MGPipeSlots().Acquire(kind, second->GetLifetimeId()); ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(secondHandle)) << kindName; EXPECT_EQ(secondHandle.Slot, firstHandle.Slot) << kindName << ": the freed slot was not handed back, so this walk did not exercise " "the recycle it exists to test"; EXPECT_NE(secondHandle.Gen, firstHandle.Gen) << kindName << ": Gen did not move on slot reuse - the predecessor's handle would " - "resolve to the successor's twin, which is the ABA the address key " - "could only paper over"; + "resolve to the successor's twin"; + auto* const secondTwin = registry.GetOrCreateByHandle(secondHandle); + ASSERT_NE(secondTwin, nullptr) << kindName; EXPECT_EQ(registry.FindByHandle(firstHandle), nullptr) << kindName << ": the STALE handle resolved to a twin"; - EXPECT_EQ(registry.FindByHandle(secondHandle), &secondTwin) << kindName; + EXPECT_EQ(registry.FindByHandle(secondHandle), secondTwin) << kindName; second.reset(); EXPECT_EQ(registry.FindByHandle(secondHandle), nullptr) << kindName; @@ -3897,18 +3804,18 @@ TEST(DirectGLESSlotTable, EverySwitchedOverKindResolvesItsTwinThroughTheHandleAr "null handle, so there is no {slot, gen} walk to drive"; } - ExpectTheHandleArmDrivesThisKind("Texture", TextureImpl::g_backendTextureObjects, [] { + ExpectTheHandleArmDrivesThisKind("Texture", MG_Pipe::MGPipeKind::Texture, TextureImpl::g_backendTextureObjects, [] { return SharedPtr(MakeShared(0u)); }); - ExpectTheHandleArmDrivesThisKind("Framebuffer", FramebufferImpl::g_backendFramebufferObjects, + ExpectTheHandleArmDrivesThisKind("Framebuffer", MG_Pipe::MGPipeKind::Framebuffer, FramebufferImpl::g_backendFramebufferObjects, [] { return MakeShared(1u); }); - ExpectTheHandleArmDrivesThisKind("Renderbuffer", RenderbufferImpl::g_backendRenderbufferObjects, + ExpectTheHandleArmDrivesThisKind("Renderbuffer", MG_Pipe::MGPipeKind::Renderbuffer, RenderbufferImpl::g_backendRenderbufferObjects, [] { return MakeShared(0u); }); - ExpectTheHandleArmDrivesThisKind("SamplerCso", SamplerImpl::g_backendSamplerObjects, + ExpectTheHandleArmDrivesThisKind("SamplerCso", MG_Pipe::MGPipeKind::SamplerCso, SamplerImpl::g_backendSamplerObjects, [] { return MakeShared(0u); }); - ExpectTheHandleArmDrivesThisKind("ShaderCso", PrgramImpl::g_backendProgramObjects, + ExpectTheHandleArmDrivesThisKind("ShaderCso", MG_Pipe::MGPipeKind::ShaderCso, PrgramImpl::g_backendProgramObjects, [] { return MakeShared(0u); }); - ExpectTheHandleArmDrivesThisKind("VertexElementsCso", VertexArrayImpl::g_backendVertexArrayObjects, + ExpectTheHandleArmDrivesThisKind("VertexElementsCso", MG_Pipe::MGPipeKind::VertexElementsCso, VertexArrayImpl::g_backendVertexArrayObjects, [] { return MakeShared(0u); }); // P3a: the SEVENTH table, and the only one the helper above cannot drive - which is the @@ -4004,10 +3911,6 @@ TEST(DirectGLESSlotTable, EverySwitchedOverKindResolvesItsTwinThroughTheHandleAr const BackendSamplerViewObject* const firstRaw = firstTwin.get(); EXPECT_EQ(table.FindByHandle(first), &firstTwin) << "SamplerViewCso: the handle does not address the twin GetOrCreate handed back"; - EXPECT_EQ(table.HandleOf(owner.get()), first) - << "SamplerViewCso: HandleOf did not resolve the view minted off this texture's " - "lifetime id, so a backend path that still arrives holding the object cannot find " - "its view"; const SharedPtr released = table.ReleaseByHandle(first); EXPECT_NE(released, nullptr) << "SamplerViewCso: delete_sampler_view found no twin to retire"; @@ -4106,21 +4009,21 @@ TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) { g_backendSamplerViews.GetOrCreate(view) = MakeShared(); ASSERT_NE(g_backendSamplerViews.FindByHandle(view), nullptr); - ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId); + ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId, view); EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr) << "the sampler-view notice left the twin behind, so the slot's next owner would " "inherit a dead texture's resolved view"; + + // The second path, in the order the client's helper runs it: the notice, then the Free + // (since P13 the notice releases by handle and never frees - the slot is the client's). + // A redundant notice before and after the Free finds nothing to release. + ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId, view); + MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, view); + ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId, view); + EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr); EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull( MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId))) - << "the notice did not return the slot"; - - // The second path. The client's helper calls Free right after raising the notice, and a - // Free on a slot that is no longer live at that generation is a proven no-op - // (SlotAllocator.cpp:117-119) - so this is the shape that actually ships, twice over. - ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId); - MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::SamplerViewCso, view); - ops->OnDestroyed(MG_Pipe::MGPipeKind::SamplerViewCso, lifetimeId); - EXPECT_EQ(g_backendSamplerViews.FindByHandle(view), nullptr); + << "the Free did not return the slot"; // And the slot really is back: a successor gets it with a moved generation, which is // the property a double free would break by skipping one. @@ -4154,15 +4057,20 @@ TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) { auto sampler = MakeShared(0u); auto program = MakeShared(0u); - TextureImpl::g_backendTextureObjects.GetOrCreate(SharedPtr(texture)); - RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreate(renderbuffer); - FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer); - SamplerImpl::g_backendSamplerObjects.GetOrCreate(sampler); - PrgramImpl::g_backendProgramObjects.GetOrCreate(program); - + // The client's mint, then the backend's adoption by that handle. const auto handleFor = [](MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) { - return MG_Pipe::MGPipeSlots().FindByLifetimeId(kind, lifetimeId); + return MG_Pipe::MGPipeSlots().Acquire(kind, lifetimeId); }; + (void)TextureImpl::g_backendTextureObjects.GetOrCreateByHandle( + handleFor(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId())); + (void)RenderbufferImpl::g_backendRenderbufferObjects.GetOrCreateByHandle( + handleFor(MG_Pipe::MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId())); + (void)FramebufferImpl::g_backendFramebufferObjects.GetOrCreateByHandle( + handleFor(MG_Pipe::MGPipeKind::Framebuffer, framebuffer->GetLifetimeId())); + (void)SamplerImpl::g_backendSamplerObjects.GetOrCreateByHandle( + handleFor(MG_Pipe::MGPipeKind::SamplerCso, sampler->GetLifetimeId())); + (void)PrgramImpl::g_backendProgramObjects.GetOrCreateByHandle( + handleFor(MG_Pipe::MGPipeKind::ShaderCso, program->GetLifetimeId())); cases.push_back({"Texture", MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId(), handleFor(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId())}); cases.push_back({"Renderbuffer", MG_Pipe::MGPipeKind::Renderbuffer, renderbuffer->GetLifetimeId(), @@ -4186,9 +4094,9 @@ TEST(DirectGLESSlotTable, ADeathNoticeForEveryP4aKindIsIdempotent) { EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull( MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId))) << one.name << ": the object's own death did not return its slot"; - // Second and third deliveries: nothing to resolve, nothing to free, no abort. - ops->OnDestroyed(one.kind, one.lifetimeId); - ops->OnDestroyed(one.kind, one.lifetimeId); + // Second and third deliveries of the dead handle: nothing to release, no abort. + ops->OnDestroyed(one.kind, one.lifetimeId, one.handle); + ops->OnDestroyed(one.kind, one.lifetimeId, one.handle); EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull( MG_Pipe::MGPipeSlots().FindByLifetimeId(one.kind, one.lifetimeId))) << one.name << ": a redundant notice resurrected a mapping"; @@ -4227,10 +4135,10 @@ TEST(DirectGLESSlotTable, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice) auto& registry = TextureImpl::g_backendTextureObjects; SharedPtr texture = MakeShared(0u); - auto& twin = registry.GetOrCreate(texture); - (void)twin; - const MG_Pipe::MGPipeHandle handle = registry.HandleOf(texture.get()); + const MG_Pipe::MGPipeHandle handle = + MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Texture, texture->GetLifetimeId()); ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(handle)); + ASSERT_NE(registry.GetOrCreateByHandle(handle), nullptr); // The fixture's shape: copy the registry while the twin is live. auto saved = registry; @@ -4241,7 +4149,6 @@ TEST(DirectGLESSlotTable, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice) EXPECT_EQ(registry.FindByHandle(handle), nullptr) << "the registry global kept the twin"; EXPECT_EQ(saved.FindByHandle(handle), nullptr) << "the saved copy kept the dead texture's twin - the dispatcher told one holder only"; - EXPECT_FALSE(registry.DestroyByLifetimeId(0)) << "sanity: a null id frees nothing"; } diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index 5fd0f889..a1e353c8 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -2343,36 +2343,6 @@ namespace { MGPipeKind::Texture>; } // namespace -TEST(RemoteGuards, SlotTableHandleOfFromTheApplyThreadIsFatalByName) { - const auto child = RunInChild([] { - StartControlSession(); - Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { - MG_State::pGLContext = MakeUnique(); - TestTextureTable table; - MG_State::GLState::TextureObject2D tex(44); - table.HandleOf(&tex); - return MOBILEGL_OK; - }, nullptr); - ClientSessionInstance().Stop(); - }); - ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); -} - -TEST(RemoteGuards, SlotTableMintingGetOrCreateFromTheApplyThreadIsFatalByName) { - const auto child = RunInChild([] { - StartControlSession(); - Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) { - MG_State::pGLContext = MakeUnique(); - TestTextureTable table; - auto tex = MakeShared(45); - table.GetOrCreate(tex); - return MOBILEGL_OK; - }, nullptr); - ClientSessionInstance().Stop(); - }); - ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); -} - // The four accessor drives share one shape: the BufferObject is constructed on the CLIENT // thread (its own resource_create publication is a legal client-side emit), and only the // accessor runs on the apply thread, where it must die at the accessor's own guard. @@ -2385,9 +2355,8 @@ MGL_BUFFER_GUARD_TEST(BufferSyncPersistentMappedRangeFromTheApplyThreadIsFatalBy MGL_BUFFER_GUARD_TEST(BufferHasDefinedContentFromTheApplyThreadIsFatalByName, 707, (void)buffer.HasDefinedContent()) #undef MGL_BUFFER_GUARD_TEST -// Historical P5d/P5e probes remain named for compatibility. P5f retires both -// scope exemptions, including barriered records; all allocator access on apply is -// client-memory access regardless of whether that memory happens to be stable. +// All allocator access on apply is client-memory access regardless of whether that memory +// happens to be stable (P5f), so the allocator refuses it by name. TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByName) { const auto child = RunInChild([] { StartControlSession(); @@ -2402,38 +2371,8 @@ TEST(RemoteGuards, AnAllocatorProbeFromTheApplyThreadOutsideEveryScopeIsFatalByN ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); } -// Construct on the client and run only HandleOf on apply. The false barrier stamp -// keeps the original P5e scenario; the tests below add the old barriered loophole. -TEST(RemoteGuards, AnAllocatorProbeInsideAnExemptionScopeFromTheApplyThreadIsFatalEvenAtTheOldDebtSites) { - struct Probe { - TestTextureTable* table; - MG_State::GLState::TextureObject2D* texture; - }; - const auto child = RunInChild([] { - StartControlSession(); - MG_State::GLState::TextureObject2D texture(46); - TestTextureTable table; - Probe probe{&table, &texture}; - Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( - +[](void* self) -> MobileGLResult { - auto& probe = *static_cast(self); - // The record being applied is one the client did NOT park behind. The stamp is - // the thread_local the sink writes (ID-103), so setting it here puts this - // thread in exactly the state an unbarriered apply is in. - MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(false); - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - probe.table->HandleOf(probe.texture); - return MOBILEGL_OK; - }, - &probe); - ClientSessionInstance().Stop(); - }); - ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); -} - -// P5f (fr): barriers and old named scopes no longer admit frontend identity. -// Construct the object and table on the client so each death comes from the exact -// probed member, not a constructor's resource_create or a missing record. +// P5f (fr): a barriered record does not admit the client allocator either (the named scopes +// that once exempted it were deleted at P13, with the last frontend-keyed backend entry). TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) { for (const auto backend : {BackendType::DirectGLES, BackendType::DirectVulkan}) { const auto child = RunInChild([backend] { @@ -2442,8 +2381,6 @@ TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) { Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult { MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); MG_Config::ActiveBackendType = *static_cast(self); - const MG_Pipe::MGPipeFrontendKeyedRegistryScope registryScope; - const MG_Pipe::MagmaP7AllocatorDebtScope magmaScope; // HighWater had no per-method guard in the old implementation. (void)MGPipeSlots().HighWater(MGPipeKind::Texture); return MOBILEGL_OK; @@ -2454,71 +2391,6 @@ TEST(RemoteGuards, BarrieredLegacyScopesCannotExemptAllocator) { } } -TEST(RemoteGuards, BarrieredFrontendRegistryMembersRefuseBothLegacyScopes) { - struct Probe { - TestTextureTable* table; - SharedPtr* texture; - MGPipeHandle handle; - int operation; - }; - const char* members[] = {"HandleOf", "GetOrCreate(StatePtr)", "NoteStateForHandle", "StateForHandle"}; - for (int operation = 0; operation != 4; ++operation) { - SCOPED_TRACE(members[operation]); - const auto child = RunInChild([operation] { - StartControlSession(); - auto texture = MakeShared(47); - TestTextureTable table; - table.GetOrCreate(texture) = MakeShared(); - Probe probe{&table, &texture, table.HandleOf(texture.get()), operation}; - Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult { - auto& probe = *static_cast(self); - MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); - MG_Config::ActiveBackendType = BackendType::DirectVulkan; - const MG_Pipe::MGPipeFrontendKeyedRegistryScope registryScope; - const MG_Pipe::MagmaP7AllocatorDebtScope magmaScope; - switch (probe.operation) { - case 0: probe.table->HandleOf(probe.texture->get()); break; - case 1: probe.table->GetOrCreate(*probe.texture); break; - case 2: probe.table->NoteStateForHandle(probe.handle, *probe.texture); break; - case 3: probe.table->StateForHandle(probe.handle); break; - } - return MOBILEGL_OK; - }, &probe); - ClientSessionInstance().Stop(); - }); - ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); - EXPECT_NE(child.Log.find(std::string{"BackendSlotTable::"} + members[operation]), std::string::npos); - } -} - -TEST(RemoteGuards, LegacyRegistryWrapperCannotExposeFrontendKeysOnApply) { - using Registry = MG_Backend::DirectGLES::StateBackendObjectRegistry< - MG_State::GLState::TextureObject2D, FakeTwin, MGPipeKind::Texture>; - struct Probe { Registry* registry; SharedPtr* texture; int operation; }; - for (int operation = 0; operation != 3; ++operation) { - const auto child = RunInChild([operation] { - StartControlSession(); - auto texture = MakeShared(48); - Registry registry; - Probe probe{®istry, &texture, operation}; - Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting(+[](void* self) -> MobileGLResult { - auto& probe = *static_cast(self); - MG_Pipe::MGPipeApplierSetCurrentRecordBarriered(true); - const MG_Pipe::MGPipeFrontendKeyedRegistryScope scope; - switch (probe.operation) { - case 0: probe.registry->Find(probe.texture->get()); break; - case 1: probe.registry->GetOrCreate(*probe.texture); break; - case 2: (void)probe.registry->begin(); break; - } - return MOBILEGL_OK; - }, &probe); - ClientSessionInstance().Stop(); - }); - ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); - EXPECT_NE(child.Log.find("BackendSlotTable::Registry."), std::string::npos); - } -} - TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) { const auto child = RunInChild([] { StartControlSession(); @@ -2547,22 +2419,6 @@ TEST(RemoteGuards, HandleRegistryMembersWorkOnBarrieredAndUnbarrieredApply) { EXPECT_EQ(WEXITSTATUS(child.Status), 0) << child.Log; } -// A marker held by the GL thread never affects the apply guard either. -TEST(RemoteGuards, AnExemptionScopeHeldOnTheGLThreadDoesNotExemptTheApplyThread) { - const auto child = RunInChild([] { - StartControlSession(); - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting( - +[](void*) -> MobileGLResult { - MG_Pipe::MGPipeRefuseAllocatorFromApplyThread("FindByLifetimeId"); - return MOBILEGL_OK; - }, - nullptr); - ClientSessionInstance().Stop(); - }); - ExpectNamedAbort(child, "Fatal{RoleViolation, \"MGPipeSlots\"}"); -} - TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) { const auto child = RunInChild([] { MG_Config::Transport = MG_Config::TransportMode::InProcess; diff --git a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp index 2c7383e9..6ed9600f 100644 --- a/MobileGL/MG_Test/Wire/ServerLoopTest.cpp +++ b/MobileGL/MG_Test/Wire/ServerLoopTest.cpp @@ -2479,49 +2479,6 @@ TEST(StagedShadowProductionTest, TheStreamingIdiomThroughAPoolReuseIsUploadedNot "whole-store refusal must key on the descriptor's HasDefinedContent); the log says: " << appended; } -namespace { - // The drive below, in a FORKED CHILD that brings the server up itself (the integration - // harness's pre-flight shape), so a Fatal on the apply thread ends the child and not the - // case. Exit 8 = the child's bring-up failed; exit 0 = the apply-thread twin mint ran, - // which is the refusal this case pins having been removed. - [[noreturn]] void RunFrontendFramebufferDeathOnApplyThreadAndExit() { - EglServerFixture fixture; - if (!fixture.BringUp().empty() || !fixture.MakeCurrent()) ::_exit(8); - namespace GLES = MG_Backend::DirectGLES; - auto framebuffer = MakeShared(901u); - (void)OnApply([&] { - auto& twin = GLES::FramebufferImpl::g_backendFramebufferObjects.GetOrCreate(framebuffer); - twin = MakeShared(); - }); - framebuffer.reset(); - ::_exit(0); - } -} // namespace - -// P5c (hd, CONTRACT-P5C §3.1 / §6 layer 1): this case's original drive - mint the framebuffer -// twin on the apply thread off the frontend object's lifetime id, then let the frontend death -// notice reach the in-process death switch there - is refused by name at the first step: -// the minting GetOrCreate(StatePtr) is monolith glue and the apply-thread call is -// Fatal{RoleViolation, "MGPipeSlots"}. The deletion-on-owner semantics it pinned return with -// ct's object_death record (the framebuffer family's first wire delete), whose sink releases -// the twin BY HANDLE. The name stays; the pin is now the refusal that stands until ct lands. -TEST(ServerLoopEglTest, FrontendFramebufferDeathDeletesOnTheContextOwner) { - MG_Config::Features.PipePush |= MG_Pipe::kMGPipeSubsystemEsprytSlots; - { - EglServerFixture probe; - MGL_EGL_BRING_UP_OR_BAIL(probe); - probe.TearDown(); - } - const SizeT mark = ReadLog().size(); - EXPECT_EXIT(RunFrontendFramebufferDeathOnApplyThreadAndExit(), ::testing::KilledBySignal(SIGABRT), ".*") - << "the apply-thread mint of a frontend framebuffer's twin was not refused; exit 8 is the " - "child's own bring-up failing"; - const std::string appended = ReadLogFrom(mark); - EXPECT_NE(appended.find("Fatal{RoleViolation, \"MGPipeSlots\"}"), std::string::npos) - << "the abort was not the allocator guard's; the log says: " << appended; -} - - #endif // !_WIN32 // ===================================================================================== diff --git a/scripts/data/link_ratchet_baseline.txt b/scripts/data/link_ratchet_baseline.txt index d5130613..f0e5c500 100644 --- a/scripts/data/link_ratchet_baseline.txt +++ b/scripts/data/link_ratchet_baseline.txt @@ -5,24 +5,14 @@ # base commit : # configure : -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DCMAKE_BUILD_TYPE=Release # build dir : /home/swung/mgl-p13-split -# count : 49 +# count : 36 # # One demangled symbol per line. A trailing ` # note` is an annotation the parser -# ignores; `# P13` marks a symbol that cannot fall before P13 establishes module -# target boundaries. The LIST is the baseline, not the count: a ratchet that +# ignores. `# P13` (debts only P13's module work could clear) is refused since +# P13 closed them. The LIST is the baseline, not the count: a ratchet that # stored only a number would go green on the day one symbol is cleared and another # added (ID-P7-7). MobileGL::MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo -MobileGL::MG_Pipe::MGPipeApplierIsUnbarrieredApply() # P13 -MobileGL::MG_Pipe::MGPipeRefuseAllocatorFromApplyThread(char const*) # P13 -MobileGL::MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread(char const*) # P13 -MobileGL::MG_Pipe::MGPipeSlotAllocator::Acquire(MobileGL::MG_Pipe::MGPipeKind, unsigned long) # P13 -MobileGL::MG_Pipe::MGPipeSlotAllocator::FindByLifetimeId(MobileGL::MG_Pipe::MGPipeKind, unsigned long) const # P13 -MobileGL::MG_Pipe::MGPipeSlotAllocator::Free(MobileGL::MG_Pipe::MGPipeKind, MobileGL::MG_Pipe::MGPipeHandle) # P13 -MobileGL::MG_Pipe::MGPipeSlots() # P13 -MobileGL::MG_Pipe::PipeInputs::GetBufferBindingPointCount(MobileGL::BufferTarget) const -MobileGL::MG_Pipe::PipeInputs::HasOpenTransformFeedbackSpan(unsigned long) const # P13 monolith arm: MGB_CTX accessor / GL readback packer (CONTRACT-P7 §12) -MobileGL::MG_Pipe::PipeInputs::InvalidateCompileEnv() # P13 monolith arm: MGB_CTX accessor / GL readback packer (CONTRACT-P7 §12) MobileGL::MG_Pipe::PipeInputs::IsLive() const MobileGL::MG_Pipe::PipeInputs::RecordError(MobileGL::ErrorCode, std::unique_ptr >) MobileGL::MG_Remote::Client::AdoptTierIsEmulate() @@ -50,14 +40,11 @@ MobileGL::MG_State::GLState::DecodeProgramArchive(unsigned char const*, unsigned MobileGL::MG_State::GLState::GetBufferBackendOps() MobileGL::MG_State::GLState::ProgramArtifactsSchemaFingerprint() MobileGL::MG_State::GLState::ProgramObject::JoinPendingLink() const -MobileGL::MG_State::GLState::RenderbufferObject::GetExternalIndex() const MobileGL::MG_State::GLState::SamplerObject::GetAllSamplerParameters() const MobileGL::MG_State::GLState::SamplerObject::GetExternalIndex() const MobileGL::MG_State::GLState::SamplerObject::GetLifetimeId() const -MobileGL::MG_State::GLState::SamplerObject::GetVersion() const MobileGL::MG_State::GLState::SetBufferBackendOps(MobileGL::MG_State::GLState::BufferBackendOps const*) MobileGL::MG_State::GLState::TextureObjectBuffer::GetBufferBindingSlot(MobileGL::TextureUploadTarget) MobileGL::MG_State::GLState::TextureUnit::GetAllBindingSlots() MobileGL::MG_State::GLState::TextureUnit::GetSamplerObject() const MobileGL::MG_State::GLState::VertexArrayObject::GetAllAttributes() const -MobileGL::MG_State::GLState::VertexArrayObject::GetAttribute(unsigned int) const diff --git a/scripts/link_ratchet.py b/scripts/link_ratchet.py index 099c2ca4..277da213 100644 --- a/scripts/link_ratchet.py +++ b/scripts/link_ratchet.py @@ -40,6 +40,11 @@ regression this gate exists to catch: new backend code reaching into a frontend symbols that disappeared, with an instruction to re-baseline in the same commit. Progress is never a red build; new reach always is. +THE P13 EXIT GATE. A baseline line annotated `# P13` named a debt that only P13's module work +could clear. P13 cleared every one, so --assert-monotone now also fails on ANY `# P13` +annotation left in (or put back into) the baseline: that class of debt has no later phase to +wait for. + GUARD RAILS. The build must be a disaggregated one (-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON; every build pushes since P13 W3a, so MOBILEGL_PIPE_PUSH is no longer an option to check): with the option OFF, MG_Remote is not compiled at all, the SERVER set @@ -176,11 +181,16 @@ A6_TOTAL = 184 # known object path rather than by splitting the whole line. NM_BODY_RE = re.compile(r"^\s*([0-9a-fA-F]*)\s+([A-Za-z?])\s(.+)$") -# A baseline line may carry a trailing ` # note` annotation (ID-P7-7 asks for `# P13` on the -# symbols that cannot fall before P13). No demangled C++ name contains " #", so splitting on +# A baseline line may carry a trailing ` # note` annotation (ID-P7-7 asked for `# P13` on the +# symbols that could not fall before P13). No demangled C++ name contains " #", so splitting on # the first occurrence is unambiguous. BASELINE_ANNOTATION = " #" + +def p13_debts(notes): + """The baseline symbols still annotated `# P13` - none may remain after P13 (exit gate).""" + return sorted(symbol for symbol, note in notes.items() if note.split()[:1] == ["P13"]) + REQUIRED_FLAGS = ( ("MOBILEGL_BUILD_DISAGGREGATED", "ON"), ("MOBILEGL_BUILD_DISAGGREGATED_INPROC", "ON"), @@ -628,6 +638,11 @@ def self_test(): problems.append("baseline annotation not read: {}".format(notes)) if len(notes) != 2: problems.append("baseline annotations: {}".format(notes)) + if p13_debts(notes) != ["MobileGL::MG_Pipe::MGPipeSlots()", + "MobileGL::Vec4::Vec4(int, int, int, int)"]: + problems.append("P13 exit gate did not name both `# P13` lines: {}".format(p13_debts(notes))) + if p13_debts({"x": "P13x", "y": "kept for P14"}): + problems.append("P13 exit gate matched a note that is not a `# P13` annotation") finally: os.unlink(tmp) @@ -808,6 +823,14 @@ def main(): say(" referred by: " + ", ".join(result["referrers"][symbol])) if args.assert_monotone and new: exit_code = 1 + p13 = p13_debts(baseline_notes) + if p13: + say("{} baseline symbol(s) still carry a `# P13` annotation; P13 closed that class of " + "debt, so none may remain:".format(len(p13))) + for symbol in p13: + say(" P13: " + symbol) + if args.assert_monotone: + exit_code = 1 if not new and not gone: say("ratchet: unchanged at {} symbol(s)".format(len(result["symbols"]))) @@ -850,7 +873,8 @@ def main(): say("json written to " + args.json) if exit_code: - say("FAIL --assert-monotone: {} new frontend symbol(s) (named above)".format(len(new))) + say("FAIL --assert-monotone: {} new frontend symbol(s), {} `# P13` annotation(s) (named " + "above)".format(len(new), len(p13_debts(baseline_notes)))) return exit_code @@ -871,8 +895,8 @@ def write_baseline(path, symbols, notes, cache_values, build_dir): "# count : {}\n" "#\n" "# One demangled symbol per line. A trailing ` # note` is an annotation the parser\n" - "# ignores; `# P13` marks a symbol that cannot fall before P13 establishes module\n" - "# target boundaries. The LIST is the baseline, not the count: a ratchet that\n" + "# ignores. `# P13` (debts only P13's module work could clear) is refused since\n" + "# P13 closed them. The LIST is the baseline, not the count: a ratchet that\n" "# stored only a number would go green on the day one symbol is cleared and another\n" "# added (ID-P7-7).\n".format( head, flags, (cache_values or {}).get("CMAKE_BUILD_TYPE", "?"), build_dir,