From 72e79020802c00fbe15ff5c8e2113d14ffb233ed Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 07:56:34 -0400 Subject: [PATCH 1/5] [MG_Backend, MG_State, MG_Remote] (Disaggregated): P5e tx2 - the texture family by handle, the unit window, and the guard that notices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle get their bodies (the c0e seams): record first, twin by GetOrCreateByHandle, three syncs from the record, no frontend touch and no allocator probe on the path. - BackendTextureObject carries m_pushedSyncHandle (CONTRACT-P5E §4.1's m_handle), which is what the three sync prologues read instead of probing the client allocator with HandleOf, and what selects the by-handle arm inside them. - IsDrawSyncCleanByRecord replaces IsDrawSyncClean(frontend) on the memo-hit path: the same conjunction over the record's serials and the twin's own, with BOTH halves of each resync pair read and neither clearing the other's (D-E2, scout R4). - The unit work list is built from st.BoundSamplerViews[] and keyed on (ContextSerial, SamplerViewsSerial, window, backend context generation); PairingsIntact is not carried on it, because a {slot, gen} has no stale-pairing hazard to catch. - ResolveAndBindUnitTextures is one pass over the window with the target from the view CSO, then an unbind driven by g_boundTexturesCache; the alias arbiter dies (the client arbitrated, SamplerEmit.h). - BindCurrentTextures' memo key is ruling 6's both halves, entry selection included. - The sampler pass reads BoundSamplerStates / the built-in sampler CSO / the view's InternalFormat; the apply-thread mint and ResolveUnitSamplerBackend become a named refusal under a transport (§4.2). - GenerateMipmap resolves VerbMipRes and never the active unit; the detach sweep is keyed by the texture handle over the applier's framebuffer records. The row is NOT flipped to kWaitNone: the RGB16F/RGB32F CPU filter still reaches the client's level shadows, so ruling 14's escape applies and the flip is trailing. - RequireImageBindableStorage gets a by-handle entry whose re-dirty is Fatal{UnmigratedEmulation, "image-bindable-redirty"} at the entry rather than three frames deep at a MarkStorageDirty guard. - The buffer-texture arm resolves scout R5 with Desc.BufferForTexBuffer / BufOffset / BufSize and EnsureBufferResourceForHandle(nullptr, h) - that function is client-free under an active transport (kimi audit row 29). - CheckUnitWindows(st) at draw and dispatch makes the §5.3 window promise checkable and checked: narrower than [0, MaxTouchedTextureUnit] is Fatal{ProtocolCorruption, "SetSamplerViews.Count"} / "BindSamplerStates.Count". - MipmapStorage's guard list gains the read-only shape accessors P5c deliberately exempted, now that nothing on the draw path reads them. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 738 +++++++++++++++--- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 508 ++++++++++-- MobileGL/MG_Backend/DirectGLES/Managers.h | 103 ++- MobileGL/MG_Remote/Server/PipeApplier.cpp | 69 ++ .../GLState/TextureState/MipmapStorage.cpp | 36 + 5 files changed, 1287 insertions(+), 167 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index ed19e4c3..5654752f 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -284,13 +284,16 @@ namespace MobileGL::MG_Backend::DirectGLES { return g_rawDepthFetchSamplerBackend.get(); } - Bool NeedsRawDepthFetchSampler(const SharedPtr& samplerObject, + // P5e (tx2), CONTRACT-P5E §5.3: the substitution's decision, over the fifteen sampler VALUES + // rather than over the object that happens to hold them. ARCHITECTURE.md §5.5 keeps the + // substitution itself on the server; this is only where its inputs come from, and the CSO + // record carries all three (MGPipeValueTypes.h). The SharedPtr overload below is the same + // three lines with one dereference in front, so the two arms cannot part. + Bool NeedsRawDepthFetchSampler(const SamplerParameters& samplerParams, TextureInternalFormat textureFormat) { - if (!MG_Util::IsDepthFormatInternalFormat(textureFormat) || !samplerObject) { + if (!MG_Util::IsDepthFormatInternalFormat(textureFormat)) { return false; } - - const auto& samplerParams = samplerObject->GetAllSamplerParameters(); if (samplerParams.compareMode != SamplerCompareMode::None) { return false; } @@ -300,6 +303,14 @@ namespace MobileGL::MG_Backend::DirectGLES { samplerParams.magFilter != SamplerFilterMode::Nearest; } + Bool NeedsRawDepthFetchSampler(const SharedPtr& samplerObject, + TextureInternalFormat textureFormat) { + if (!samplerObject) { + return false; + } + return NeedsRawDepthFetchSampler(samplerObject->GetAllSamplerParameters(), textureFormat); + } + // Frontend texture target a GLSL sampler uniform samples from. Only used to find // which of a unit's bindings carries the GL_TEXTURE_LOD_BIAS the shader needs; // targets with no mip chain map to Unknown so the lookup falls back to no bias. @@ -2023,6 +2034,25 @@ namespace MobileGL::MG_Backend::DirectGLES { // mode here - a silent permanent fallback is, and that is what the line is for. // The caller's MINOR-4 gate tick still counts EVERY decline, loud or not. if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.3: UNDER A LIVE WIRE THE DECLINE IS A REFUSAL. + // + // The fall-back below is the pre-handle snapshot walk over GetTextureUnitObject, + // and on a run-ahead server that walk reads client memory for a record that has + // already been answered - there is no wait left in which the answer could be + // right. Ruling 19 makes the missing window unrepresentable at the sink; this is + // the same statement one level down, for the backend that would have papered over + // it. Under monolith the fall-back is correct and stays. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && maxTouchedUnit >= 0) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - a draw " + "touches units 0..%d and neither a sampler-view nor a sampler-state " + "window has ever been applied while the sampler subsystem bit is set; " + "the pre-handle unit-bindings snapshot walk reads client memory and is " + "refused under an active transport (CONTRACT-P5E.md §5.3)", + static_cast(maxTouchedUnit)); + std::abort(); + } +#endif if (maxTouchedUnit >= 0) { MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw " "touches units 0..%d and neither a sampler-view nor a sampler-state " @@ -2137,6 +2167,49 @@ namespace MobileGL::MG_Backend::DirectGLES { } return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.8 / ruling 1: THE ARM SELECTOR for every by-handle site in + // this family. Under ANY active transport (lockstep or run-ahead, so RUN_AHEAD=0 stays a + // pure wait-rule A/B on identical server code), and only with BOTH family bits set: a + // unit's texture comes from the sampler-view window and its storage from the + // texture-resource record, and half of each is not an arm. The push-monolith build keeps + // the frontend walk token for token - there is no server to answer from there. + static Bool UnitTexturesByHandle() { + return MG_Config::Transport != MG_Config::TransportMode::Monolith && SamplerSubsystemEnabled() && + TextureResourceSubsystemEnabled(); + } + + // P5e (tx2), CONTRACT-P5E §5.2: THE UNIT WORK LIST WITH NO BORROWED SLOT IN IT. + // + // The pre-P5e entry borrows a pointer INTO a frontend binding slot and remembers which + // frontend object it was paired with, and PairingsIntact re-checks that pairing before + // every replay. Both exist because the list's key is derived state and a silent slot swap + // could leave a stale pairing driving texture A's twin from texture B's frontend state. + // + // A HANDLE HAS NO SUCH HAZARD. `Res` is {slot, gen}: a recycled slot arrives at a higher + // Gen, which the twin table's forward-Gen rule answers by RESETTING the twin, and a + // backward Gen is refused outright - so there is nothing for a pairing check to catch and + // PairingsIntact is not carried on this list at all. The key is the applier's own + // (ContextSerial, SamplerViewsSerial, window) plus the backend context generation, and + // SamplerViewsSerial moves IFF the resolved per-unit view set moved, which the emitter's + // content-hash suppressor is what makes sound. + // + // Its OWN list and its own key rather than a re-shape of UnitTextureSyncEntry, because + // that struct is also the draw-FBO and read-FBO attachment lists' (fb's), and one name + // carrying two shapes across two packages' worktrees is the merge trap the contract + // package exists to avoid. + struct UnitTextureSyncByHandleEntry { + MG_Pipe::MGPipeHandle Res = MG_Pipe::kMGPipeNullHandle; + BackendTextureObject* backend = nullptr; + }; + static Vector g_unitTextureSyncListByHandle; + static Bool g_unitTextureSyncListByHandleValid = false; + static Uint64 g_unitTextureSyncListByHandleContextSerial = 0; + static Uint64 g_unitTextureSyncListByHandleViewsSerial = 0; + static Uint32 g_unitTextureSyncListByHandleStart = 0; + static Uint32 g_unitTextureSyncListByHandleCount = 0; + static Uint g_unitTextureSyncListByHandleContextGeneration = 0; +#endif static Vector g_unitTextureSyncList; static Bool g_unitTextureSyncListValid = false; static Uint64 g_unitTextureSyncListContextId = 0; @@ -2332,6 +2405,68 @@ namespace MobileGL::MG_Backend::DirectGLES { const Int maxTouchedUnit = keys.maxTouchedUnit; const Uint64 samplingGeneration = keys.samplingGeneration; const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.2. THE UNIT HALF, BY HANDLE. + // + // set_sampler_views is the RESOLVED answer to the question this walk asks - which + // texture does each touched unit sample - and the client has already applied every + // drop the walk below performs by hand: an image-less default texture and a texture + // that samples as incomplete are both null in the record (SamplerEmit.h's two drops), + // and the alias contest between two real textures on one native target was arbitrated + // by the program's own sampler types. So the window IS the work list. + // + // NAMED BEHAVIOUR DELTA (P5e-5, contract §5.2): this syncs the PROGRAM-RESOLVED + // texture per unit, where the frontend walk syncs every slot of every touched unit. + // Coverage is unchanged - views UNION image units UNION the FBO attachment lists + // UNION the waited texture ops still cover every texture anything reads - so it + // narrows WORK and not what reaches the driver. + if (UnitTexturesByHandle()) { + const auto& st = MG_Pipe::MGPipeApplier(); + const Bool listValid = g_unitTextureSyncListByHandleValid && + g_unitTextureSyncListByHandleContextSerial == st.ContextSerial && + g_unitTextureSyncListByHandleViewsSerial == st.SamplerViewsSerial && + g_unitTextureSyncListByHandleStart == st.SamplerViewStart && + g_unitTextureSyncListByHandleCount == st.SamplerViewCount && + g_unitTextureSyncListByHandleContextGeneration == g_backendContextGeneration; + if (listValid) { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/true); + } + for (const auto& entry : g_unitTextureSyncListByHandle) { + const auto* record = PipeTextureRecordForHandle(entry.Res); + if (record == nullptr) continue; + // The aggregate gate, from the twin's own serials against the record's - + // no frontend version, no context id, no sampling generation. + if (entry.backend->IsDrawSyncCleanByRecord(entry.Res, *record)) continue; + SyncTextureToBackendByHandle(entry.Res); + } + } else { + if (MG_Util::PipeStats::Enabled()) { + MG_Util::PipeStats::CountGate(MG_Util::PipeStats::Gate::EsprytTextureSyncList, /*hit=*/false); + } + g_unitTextureSyncListByHandleValid = false; + g_unitTextureSyncListByHandle.clear(); + const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount; + for (Uint32 index = st.SamplerViewStart; + index < windowEnd && index < st.BoundSamplerViews.size(); ++index) { + const auto& view = st.BoundSamplerViews[index]; + if (MG_Pipe::MGPipeHandleIsNull(view.Texture)) continue; + auto& twin = SyncTextureToBackendByHandle(view.Texture); + if (!twin) continue; + g_unitTextureSyncListByHandle.push_back({view.Texture, twin.get()}); + } + g_unitTextureSyncListByHandleContextSerial = st.ContextSerial; + g_unitTextureSyncListByHandleViewsSerial = st.SamplerViewsSerial; + g_unitTextureSyncListByHandleStart = st.SamplerViewStart; + g_unitTextureSyncListByHandleCount = st.SamplerViewCount; + g_unitTextureSyncListByHandleContextGeneration = g_backendContextGeneration; + g_unitTextureSyncListByHandleValid = true; + } + // The pre-handle list must not be replayed after this arm ran: its entries borrow + // frontend slots this arm never refreshed. + g_unitTextureSyncListValid = false; + } else +#endif // The epoch survives redundant re-binds; the sampling-resolution generation // covers the one membership input the epoch cannot see - a default texture's // image appearing or vanishing flips IsUndefinedDefaultTexture with no binding @@ -4839,6 +4974,82 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedNC("ResolveAndBindUnitTextures", TRACY_ZONECOLOR_BACKEND); #endif Bool fullyResolved = true; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.2. ONE PASS OVER THE SAMPLER-VIEW WINDOW, AND THEN AN + // UNBIND OF EVERY TARGET THE WINDOW DID NOT CLAIM. + // + // The three-pass slot walk below exists to arbitrate: desktop 1D/1D-array targets alias + // ES 2D/2D-array, a unit can hold a real texture on one of an aliased pair and a default + // object on the other, and two REAL textures can want one native target - which the + // program's sampler types decide. THE CLIENT HAS ALREADY DECIDED ALL OF IT + // (SamplerEmit.h's per-unit resolution is keyed on exactly those sampler types), so the + // arbiter dies here rather than being re-implemented over records: one texture per unit, + // named by the record, with its frontend target in the view CSO. + // + // The unbind half keeps its shape and changes its driver: instead of walking the frontend + // unit's binding slots it walks the fixed TextureTarget enum against g_boundTexturesCache, + // which is SERVER memory and is the shadow every redundant-bind filter in this backend + // already trusts. That is what makes "the window did not claim it" mean "unbind it" - the + // same statement the pre-handle pass makes with `!boundBackendTargets[...]`. + if (TextureImpl::UnitTexturesByHandle()) { + const auto& st = MG_Pipe::MGPipeApplier(); + const Uint32 windowEnd = st.SamplerViewStart + st.SamplerViewCount; + for (Int unit = 0; unit <= maxTouchedUnit; ++unit) { + const Uint32 index = static_cast(unit); + Array boundBackendTargets{}; + if (index >= st.SamplerViewStart && index < windowEnd && index < st.BoundSamplerViews.size()) { + const auto& view = st.BoundSamplerViews[index]; + if (!MG_Pipe::MGPipeHandleIsNull(view.Texture)) { + // The TARGET is the view CSO's, which is the frontend target the client + // resolved this unit against - not a second derivation from the texture's + // descriptor, which would be a second authority for one value. + const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View); + const auto target = viewRecord != nullptr + ? static_cast(viewRecord->View.Target) + : TextureTarget::Unknown; + if (viewRecord == nullptr) { + MGLOG_E_ONCE("MGPipe: unit %d's sampler view {%u, %u} has no applier record, " + "so the target to bind texture {%u, %u} at is unknown; the unit " + "keeps what it holds", + static_cast(unit), view.View.Slot, view.View.Gen, + view.Texture.Slot, view.Texture.Gen); + fullyResolved = false; + } else if (!TextureImpl::IsSupportedTextureTarget(target)) { + MGLOG_D(" Texture target %s is not supported, skipping.", + MG_Util::ConvertTextureTargetToString(target).c_str()); + } else if (auto* twin = TextureImpl::ResolveTextureTwin(view.Texture)) { + const GLenum targetGL = TextureImpl::ConvertTextureTargetToBackendGLEnum(target); + twin->Bind(targetGL, unit); + boundBackendTargets[static_cast( + TextureImpl::MapToBackendTextureTarget(target))] = true; + } else { + // No twin yet: the sync pass that would have built it declined and + // named why. Not memoisable - a later draw would bind it without any + // key moving. + fullyResolved = false; + } + } + } + // The backend half of glBindTexture(..., 0) for every native target this unit no + // longer claims, driven by the server's own binding shadow. + Array visitedBackendTargets{}; + for (SizeT t = 0; t < (SizeT)TextureTarget::TextureTargetCount; ++t) { + const auto target = static_cast(t); + if (!TextureImpl::IsSupportedTextureTarget(target)) continue; + const auto backendTargetIndex = + static_cast(TextureImpl::MapToBackendTextureTarget(target)); + if (visitedBackendTargets[backendTargetIndex]) continue; + visitedBackendTargets[backendTargetIndex] = true; + if (boundBackendTargets[backendTargetIndex]) continue; + if (TextureImpl::g_boundTexturesCache[static_cast(unit)][backendTargetIndex] == nullptr) { + continue; + } + TextureImpl::UnbindTexture(unit, TextureImpl::ConvertTextureTargetToBackendGLEnum(target)); + } + } + return fullyResolved; + } +#endif // Frontend target the current program samples at a given unit; resolves an // aliased native binding when two real textures compete for it (see below). // Only consulted on a conflict, so the ordinary unit costs nothing. @@ -4988,8 +5199,27 @@ namespace MobileGL::MG_Backend::DirectGLES { static SamplerImpl::BackendSamplerObject* ResolveUnitSamplerBackend( Int unit, const SharedPtr& samplerObject) { #if MOBILEGL_BUILD_DISAGGREGATED + // 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. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + 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 - P3b/P4b rekeys the registry onto handles. + // scope - the refusal above is what retires it under a transport. const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; #endif auto& memo = g_unitSamplerLookupMemos[static_cast(unit)]; @@ -5099,6 +5329,20 @@ namespace MobileGL::MG_Backend::DirectGLES { // empty window is the correct emission. All 161 hits of the unnarrowed line were // that shape - the log said `units 0..-1` on every one of them. if (st.SamplerStateCount == 0 && maxTouchedUnit >= 0) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.3: the same refusal CurrentUnitBindingsEpoch makes - + // the frontend walk below reads GetTextureUnitObject per unit, which an active + // transport may not do for a record whose client has moved on. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - a draw " + "touches units 0..%d and no bind_sampler_states has ever been applied " + "while the sampler subsystem bit is set; the pre-handle sampler walk " + "reads client memory and is refused under an active transport " + "(CONTRACT-P5E.md §5.3)", + static_cast(maxTouchedUnit)); + std::abort(); + } +#endif MGLOG_E_ONCE("A sampler record does not describe the binding it names: a draw touches " "units 0..%d and no bind_sampler_states has ever been applied while the " "sampler subsystem bit is set; running the pre-handle sampler walk.", @@ -5222,6 +5466,24 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint32 programBackendStateVersion = 0; Bool programLinked = false; Uint contextGeneration = 0; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.3 / ruling 6 (ID-86): THE KEY IS BOTH HALVES. + // + // S2 argued SamplerViewsSerial alone - it already mixes the program-resolved opaque units, + // so a resolution that moved moves it - and S4 argued the program's rows. The integrator + // ruled BOTH, for the case neither covers alone: a RELINK whose resolution happens to be + // unchanged still needs the twin's new unit assignments, and SamplerViewsSerial does not + // move for it (the emitter's content hash is over the resolved set, which did not change). + // + // The frontend program rows above (pointer, lifetime id, backend-state version, link + // status) stay for the monolith arm and are simply not read on this one. + MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle; + Uint64 shaderCsoSerial = 0; + Uint64 bindingsSerial = 0; + Uint64 samplerViewsSerial = 0; + Uint64 contextSerial = 0; + Bool byHandle = false; +#endif decltype(TextureImpl::g_boundTexturesCache) boundTextures{}; }; // Small per-PROGRAM memo set, not one global: the program is part of the key @@ -5250,6 +5512,27 @@ namespace MobileGL::MG_Backend::DirectGLES { // Units past the frontend's high-water mark have provably-empty slots. const Int maxTouchedUnit = keys.maxTouchedUnit; +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2) / ruling 6: on the handle arm the key is the applier's rows and nothing + // frontend - INCLUDING the entry selection, which used the frontend program's address. + // maxTouchedUnit stays in it because the unbind sweep this memo replays is bounded by it. + const Bool byHandle = TextureImpl::UnitTexturesByHandle(); + MG_Pipe::MGPipeHandle drawProgram = MG_Pipe::kMGPipeNullHandle; + Uint64 shaderCsoSerial = 0; + Uint64 bindingsSerial = 0; + Uint64 samplerViewsSerial = 0; + Uint64 contextSerial = 0; + if (byHandle) { + const auto& st = MG_Pipe::MGPipeApplier(); + drawProgram = st.DrawProgram; + samplerViewsSerial = st.SamplerViewsSerial; + contextSerial = st.ContextSerial; + if (const auto* cso = PipeShaderCsoRecordForHandle(drawProgram)) { + shaderCsoSerial = cso->Serial; + bindingsSerial = cso->BindingsSerial; + } + } +#endif // Entry selection by program pointer; a missing program takes the round-robin // victim. WHICH entry is used is only a performance choice - correctness sits // entirely in the full key + shadow compare below, unchanged from the single @@ -5257,6 +5540,16 @@ namespace MobileGL::MG_Backend::DirectGLES { const void* programKey = static_cast(currentProgram.get()); ResolvedTextureBindingMemo* memoSlot = nullptr; for (auto& candidate : g_resolvedTextureBindingMemos) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (byHandle) { + if (candidate.valid && candidate.byHandle && candidate.drawProgram.Slot == drawProgram.Slot && + candidate.drawProgram.Gen == drawProgram.Gen) { + memoSlot = &candidate; + break; + } + continue; + } +#endif if (candidate.valid && candidate.program == programKey) { memoSlot = &candidate; break; @@ -5271,7 +5564,16 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT shadowBytes = static_cast(maxTouchedUnit + 1) * sizeof(TextureImpl::g_boundTexturesCache[0]); const Uint64 unitBindingsEpoch = keys.unitBindingsEpoch; - const Bool keysMatch = memo.valid && memo.glContextId == keys.contextId && +#if MOBILEGL_BUILD_DISAGGREGATED + const Bool keysMatch = + byHandle ? (memo.valid && memo.byHandle && memo.maxTouchedUnit == maxTouchedUnit && + memo.contextSerial == contextSerial && memo.samplerViewsSerial == samplerViewsSerial && + memo.drawProgram.Slot == drawProgram.Slot && memo.drawProgram.Gen == drawProgram.Gen && + memo.shaderCsoSerial == shaderCsoSerial && memo.bindingsSerial == bindingsSerial && + memo.contextGeneration == g_backendContextGeneration) + : +#endif + (memo.valid && memo.glContextId == keys.contextId && memo.maxTouchedUnit == maxTouchedUnit && memo.unitBindingsEpoch == unitBindingsEpoch && memo.samplingResolutionGeneration == keys.samplingGeneration && @@ -5280,7 +5582,7 @@ namespace MobileGL::MG_Backend::DirectGLES { memo.programBackendStateVersion == (currentProgram ? currentProgram->GetBackendStateVersion() : 0) && memo.programLinked == (currentProgram && currentProgram->GetLinkStatus()) && - memo.contextGeneration == g_backendContextGeneration; + memo.contextGeneration == g_backendContextGeneration); // Short-circuited: the shadow compare is only meaningful once the key (and with it the // snapshotted row count) matches. if (!keysMatch || std::memcmp(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), @@ -5296,6 +5598,14 @@ namespace MobileGL::MG_Backend::DirectGLES { memo.programBackendStateVersion = currentProgram ? currentProgram->GetBackendStateVersion() : 0; memo.programLinked = currentProgram && currentProgram->GetLinkStatus(); memo.contextGeneration = g_backendContextGeneration; +#if MOBILEGL_BUILD_DISAGGREGATED + memo.byHandle = byHandle; + memo.drawProgram = drawProgram; + memo.shaderCsoSerial = shaderCsoSerial; + memo.bindingsSerial = bindingsSerial; + memo.samplerViewsSerial = samplerViewsSerial; + memo.contextSerial = contextSerial; +#endif std::memcpy(memo.boundTextures.data(), TextureImpl::g_boundTexturesCache.data(), shadowBytes); memo.valid = true; } @@ -5559,6 +5869,84 @@ namespace MobileGL::MG_Backend::DirectGLES { samplerBinding.lastAssignedUnit = unit; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.3. THE SAMPLER PASS, FROM RECORDS. + // + // Four frontend reads used to live in this loop body and every one of + // them has a carrier: the unit's sampler object is + // st.BoundSamplerStates[u] (null = the texture's built-in sampler, which + // is TextureResources[view.Texture].Params.BuiltinSampler); its fifteen + // values - lodBias among them - are the CSO record's + // (MGPipeValueTypes.h); and the sampled texture's FORMAT, which decides + // the raw-depth-fetch substitution, is + // SamplerViewCsos[View].View.InternalFormat. + // + // THE SUBSTITUTION ITSELF STAYS ON THE SERVER (ARCHITECTURE.md §5.5 + // assigns it there by name); what moves is where its two inputs come + // from. + if (TextureImpl::UnitTexturesByHandle()) { + const auto& st = MG_Pipe::MGPipeApplier(); + const auto index = static_cast(unit); + const Bool inViewWindow = index >= st.SamplerViewStart && + index - st.SamplerViewStart < st.SamplerViewCount && + index < st.BoundSamplerViews.size(); + const MG_Pipe::MGPBoundView view = + inViewWindow ? st.BoundSamplerViews[index] : MG_Pipe::MGPBoundView{}; + const Bool inStateWindow = index >= st.SamplerStateStart && + index - st.SamplerStateStart < st.SamplerStateCount && + index < st.BoundSamplerStates.size(); + const MG_Pipe::MGPipeHandle unitSampler = + inStateWindow ? st.BoundSamplerStates[index] : MG_Pipe::kMGPipeNullHandle; + + // The EFFECTIVE sampler of the unit: the bound CSO if there is one, + // else the sampled texture's built-in sampler CSO - exactly the + // override order GL states and the pre-handle body spells with + // `samplerObject ? ... : texture->GetSamplerObject()`. + MG_Pipe::MGPipeHandle effectiveCso = unitSampler; + if (MG_Pipe::MGPipeHandleIsNull(effectiveCso) && + !MG_Pipe::MGPipeHandleIsNull(view.Texture)) { + if (const auto* texRecord = PipeTextureRecordForHandle(view.Texture)) { + effectiveCso = texRecord->Params.BuiltinSampler; + } + } + const MG_Pipe::MGPipeSamplerCsoRecord* effectiveCsoRecord = + PipeSamplerCsoRecordForHandle(effectiveCso); + + if (samplerBinding.lodBiasLocation >= 0) { + const Float lodBias = + effectiveCsoRecord != nullptr ? effectiveCsoRecord->Params.lodBias : 0.0f; + if (lodBias != samplerBinding.lastAssignedLodBias) { + g_GLESFuncs.glUniform1f(samplerBinding.lodBiasLocation, lodBias); + samplerBinding.lastAssignedLodBias = lodBias; + } + } + + const auto* viewRecord = PipeSamplerViewRecordForHandle(view.View); + const Bool sampledIsTexture2D = + viewRecord != nullptr && + static_cast(viewRecord->View.Target) == TextureTarget::Texture2D; + if (samplerBinding.uniformType == GL_SAMPLER_2D && sampledIsTexture2D && + effectiveCsoRecord != nullptr && + NeedsRawDepthFetchSampler( + effectiveCsoRecord->Params, + static_cast(viewRecord->View.InternalFormat))) { + GetRawDepthFetchSampler()->Bind(unit); + MGLOG_D("Using raw depth fetch sampler on unit %d.", unit); + } else if (!MG_Pipe::MGPipeHandleIsNull(unitSampler)) { + // THE MINT IS GONE (§4.2): the unit's sampler is the CSO's own + // twin, the same one BindCurrentUnitSamplers binds, so the two + // arms cannot hand a unit back and forth between two driver + // samplers - and there is no arm left that creates a twin from a + // frontend SamplerObject. + if (auto* csoTwin = SamplerImpl::ResolveSamplerCsoTwin(unitSampler)) { + csoTwin->Bind(unit); + } + } else { + SamplerImpl::UnbindSampler(unit); + } + continue; + } +#endif auto& textureUnit = MGB_CTX->GetTextureUnitObject(unit); auto& samplerObject = textureUnit.GetSamplerObject(); const auto& texture2D = @@ -8725,6 +9113,55 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2): the inverse of MG_Pipe::MGPipeResourceTargetForTextureTarget, for the two + // descriptor reads this file makes (Managers.cpp has the same inverse beside the storage + // sync; it is file-local there, and a header for eleven cases would be the wrong trade). + static TextureTarget TextureTargetForPipeResourceTarget(Uint8 pipeResourceTarget) { + switch (static_cast(pipeResourceTarget)) { + case MG_Pipe::MGPipeResourceTarget::Tex1D: return TextureTarget::Texture1D; + case MG_Pipe::MGPipeResourceTarget::Tex2D: return TextureTarget::Texture2D; + case MG_Pipe::MGPipeResourceTarget::Tex3D: return TextureTarget::Texture3D; + case MG_Pipe::MGPipeResourceTarget::Tex1DArray: return TextureTarget::Texture1DArray; + case MG_Pipe::MGPipeResourceTarget::Tex2DArray: return TextureTarget::Texture2DArray; + case MG_Pipe::MGPipeResourceTarget::TexCube: return TextureTarget::TextureCubeMap; + case MG_Pipe::MGPipeResourceTarget::TexCubeArray: return TextureTarget::TextureCubeMapArray; + case MG_Pipe::MGPipeResourceTarget::Tex2DMS: return TextureTarget::Texture2DMultisample; + case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray; + case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle; + case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer; + default: return TextureTarget::Unknown; + } + } + + // P5c (hd), P5e (tx2): the verification half, with the RECORD passed in instead of re-read + // from the verb stash - the caller has it and the two must not disagree about which texture + // is being verified. + static void EnsureGenerateMipmapStorageDescribed(const MG_Pipe::MGPipeResourceRecord& record) { + const auto& desc = record.Desc; + if (desc.Width == 0 || desc.Levels == 0) { + MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); + } + Uint maxDimension = desc.Width; + const auto target = static_cast(desc.Target); + if (target != MG_Pipe::MGPipeResourceTarget::Tex1D && + target != MG_Pipe::MGPipeResourceTarget::Tex1DArray) { + maxDimension = std::max(maxDimension, desc.Height); + } + if (target == MG_Pipe::MGPipeResourceTarget::Tex3D) { + maxDimension = std::max(maxDimension, desc.Depth); + } + Uint requiredLevels = 1; + while (maxDimension > 1) { + maxDimension /= 2; + ++requiredLevels; + } + if (desc.Levels < requiredLevels) { + MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); + } + } +#endif + static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr& texture) { #if MOBILEGL_BUILD_DISAGGREGATED if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { @@ -8735,27 +9172,10 @@ namespace MobileGL::MG_Backend::DirectGLES { // (MGPMipPlan::Res, the verb stash) - the client allocator is never probed (T2). const auto handle = MG_Pipe::MGPipeApplier().VerbMipRes; const auto* record = PipeTextureRecordForHandle(handle); - if (record == nullptr || record->Desc.Width == 0 || record->Desc.Levels == 0) { - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); - } - const auto& desc = record->Desc; - Uint maxDimension = desc.Width; - const auto target = static_cast(desc.Target); - if (target != MG_Pipe::MGPipeResourceTarget::Tex1D && - target != MG_Pipe::MGPipeResourceTarget::Tex1DArray) { - maxDimension = std::max(maxDimension, desc.Height); - } - if (target == MG_Pipe::MGPipeResourceTarget::Tex3D) { - maxDimension = std::max(maxDimension, desc.Depth); - } - Uint requiredLevels = 1; - while (maxDimension > 1) { - maxDimension /= 2; - ++requiredLevels; - } - if (desc.Levels < requiredLevels) { + if (record == nullptr) { MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage"); } + EnsureGenerateMipmapStorageDescribed(*record); return false; // No server-side shadow allocation was necessary. } #endif @@ -8825,6 +9245,80 @@ namespace MobileGL::MG_Backend::DirectGLES { class ScopedDetachedTextureFramebufferAttachments { public: +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2). THE SAME SWEEP, KEYED BY THE TEXTURE'S HANDLE. + // + // The frontend constructor below answers "which framebuffers attach this texture" by + // walking every live FBO twin's frontend FramebufferObject and comparing attachment + // SharedPtrs - kimi audit row 63, an unmemoised walk of client objects. The applier + // already holds the answer: every framebuffer's record carries its eleven MGPSurfaces + // and each one names its texture BY HANDLE (`MGPSurface::Res`), so the comparison is + // {slot, gen} against {slot, gen} and the level / layered / upload-target details the + // detach needs come off the same surface. + // + // This is the same reverse question fb's texture -> FBO-slot index answers for its + // attachment sync; here it is asked over the records directly, because the walk is on a + // rare path (per glGenerateMipmap) and does not need an index to be cheap. + explicit ScopedDetachedTextureFramebufferAttachments(MG_Pipe::MGPipeHandle textureHandle) { + if (MG_Pipe::MGPipeHandleIsNull(textureHandle)) return; + auto* twin = TextureImpl::ResolveTextureTwin(textureHandle); + if (twin == nullptr) return; + const GLuint backendTextureId = twin->GetBackendTextureId(); + const auto& st = MG_Pipe::MGPipeApplier(); + + FramebufferImpl::g_backendFramebufferObjects.ForEachLive( + [&](MG_Pipe::MGPipeHandle fbo, + const SharedPtr& backendFBO) { + if (!backendFBO) return; + const auto* record = st.FramebufferRecordFor(fbo); + if (record == nullptr || record->IsDefault != 0) return; + + const auto detachPoint = [&](const MG_Pipe::MGPSurface& surface, + FramebufferAttachmentType frontendType) { + if (surface.Kind != MG_Pipe::kMGPipeSurfaceKindTexture) return; + if (surface.Res.Slot != textureHandle.Slot || surface.Res.Gen != textureHandle.Gen) { + return; + } + GLenum backendAttachment = GL_NONE; + if (frontendType >= FramebufferAttachmentType::Color0 && + frontendType <= FramebufferAttachmentType::Color31) { + backendAttachment = backendFBO->GetBackendAttachmentType(frontendType); + } else { + backendAttachment = MG_Util::ConvertFramebufferAttachmentTypeToGLEnum(frontendType); + } + if (backendAttachment == GL_NONE || backendAttachment == GL_UNKNOWN_MGL) return; + + GLenum textureTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum( + static_cast(surface.UploadTarget)); + if (textureTarget == GL_UNKNOWN_MGL) { + textureTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum( + static_cast(surface.TextureTarget)); + } + const GLuint backendFBOId = backendFBO->GetBackendFramebufferId(); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, backendFBOId); + if (surface.Layered != 0) { + g_GLESFuncs.glFramebufferTexture(GL_DRAW_FRAMEBUFFER, backendAttachment, 0, 0); + } else { + g_GLESFuncs.glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, backendAttachment, + textureTarget, 0, 0); + } + ClearGLErrors(); + m_detachedAttachments.push_back({backendFBOId, backendAttachment, textureTarget, + backendTextureId, static_cast(surface.Level), + surface.Layered != 0}); + }; + + for (SizeT i = 0; i < 8; ++i) { + detachPoint(record->Color[i], + static_cast( + static_cast(FramebufferAttachmentType::Color0) + i)); + } + detachPoint(record->Depth, FramebufferAttachmentType::Depth); + detachPoint(record->Stencil, FramebufferAttachmentType::Stencil); + }); + } +#endif + explicit ScopedDetachedTextureFramebufferAttachments( const SharedPtr& texture) { if (texture == nullptr) { @@ -9146,6 +9640,46 @@ namespace MobileGL::MG_Backend::DirectGLES { g_GLESFuncs.glBindTexture(dstTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0); } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2): the two CPU-blit mip chains, driven by the RECORD the verb named. What changes + // against the P5c arms below is only the resolution: those took the frontend texture and + // re-derived its handle with a scoped `HandleOf` probe (the two sites §4.4 listed as + // barriered), and the handle is now simply passed in - the extent derivation, the per-level + // blits and the format question are §1's descriptor reads exactly as P5c wrote them. + static void GenerateDepthTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture) { + const auto& desc = record.Desc; + const GLuint textureId = backendTexture->GetBackendTextureId(); + for (Uint32 level = 1; level < desc.Levels; ++level) { + const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level - 1); + const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level); + BlitDepthTexture2D(textureId, static_cast(level - 1), 0, 0, + static_cast(srcSize.x()), static_cast(srcSize.y()), textureId, + static_cast(level), 0, 0, static_cast(dstSize.x()), + static_cast(dstSize.y())); + } + } + + static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture) { + const auto& desc = record.Desc; + const GLenum filter = + IsIntegerColorFormat(static_cast(desc.InternalFormat)) ? GL_NEAREST : GL_LINEAR; + const GLuint textureId = backendTexture->GetBackendTextureId(); + for (Uint32 level = 1; level < desc.Levels; ++level) { + const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level - 1); + const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, + desc.Depth, level); + BlitColorTexture2D(textureId, static_cast(level - 1), 0, 0, static_cast(srcSize.x()), + static_cast(srcSize.y()), textureId, static_cast(level), 0, 0, + static_cast(dstSize.x()), static_cast(dstSize.y()), filter); + } + } +#endif + static void GenerateDepthTexture2DMipmap( const SharedPtr& texture, const SharedPtr& backendTexture) { @@ -9158,36 +9692,10 @@ namespace MobileGL::MG_Backend::DirectGLES { auto* mipmapTexture = dynamic_cast(texture.get()); MOBILEGL_ASSERT(mipmapTexture != nullptr, "Depth mipmap generation requires mipmap storage."); -#if MOBILEGL_BUILD_DISAGGREGATED - // P5c (tx): the per-level extents are §1's derivation from the descriptor under an - // active transport - GetMipmapTexelSize is the client's per-level shape and the apply - // thread may not name it (rule E). Texture2D only, so x and y shrink and z stays 1. - // The record resolution is the same registry lookup texture sync and - // EnsureGenerateMipmapStorageAllocated's disaggregated arm already make. - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside - // the scope - P3b/P4b rekeys the registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get()); - const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle); - if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) { - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-shape"); - } - const auto& desc = pushedRecord->Desc; - const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint32 level = 1; level < desc.Levels; ++level) { - const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent( - desc.Target, desc.Width, desc.Height, desc.Depth, level - 1); - const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent( - desc.Target, desc.Width, desc.Height, desc.Depth, level); - BlitDepthTexture2D(textureId, static_cast(level - 1), 0, 0, - static_cast(srcSize.x()), static_cast(srcSize.y()), - textureId, static_cast(level), 0, 0, - static_cast(dstSize.x()), static_cast(dstSize.y())); - } - return; - } -#endif + // P5e (tx2): the transport arm that stood here - a scoped `HandleOf(texture.get())` + // probe to re-derive a handle the generate_mipmap record already carried (CONTRACT-P5E + // §4.4's site 9161) - is GenerateDepthTexture2DMipmapByRecord above, reached from + // GenerateMipmapByRecord with the handle passed in. This overload is the monolith body. const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount(); MOBILEGL_ASSERT(mipLevelCount > 0, "Depth mipmap generation requires allocated storage."); @@ -9215,37 +9723,8 @@ namespace MobileGL::MG_Backend::DirectGLES { auto* mipmapTexture = dynamic_cast(texture.get()); MOBILEGL_ASSERT(mipmapTexture != nullptr, "Color mipmap generation requires mipmap storage."); -#if MOBILEGL_BUILD_DISAGGREGATED - // P5c (tx): GenerateDepthTexture2DMipmap's descriptor arm, for the color filter path - - // same §1 extent derivation, same registry resolution, same refusal when the handle - // arm has no record to read. - if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed twin resolution, named debt inside - // the scope - P3b/P4b rekeys the registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; - const auto pushedHandle = TextureImpl::g_backendTextureObjects.HandleOf(texture.get()); - const auto* pushedRecord = PipeTextureRecordForHandle(pushedHandle); - if (pushedRecord == nullptr || pushedRecord->Desc.Width == 0 || pushedRecord->Desc.Levels == 0) { - MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-shape"); - } - const auto& desc = pushedRecord->Desc; - const GLenum filter = - IsIntegerColorFormat(static_cast(desc.InternalFormat)) ? GL_NEAREST - : GL_LINEAR; - const GLuint textureId = backendTexture->GetBackendTextureId(); - for (Uint32 level = 1; level < desc.Levels; ++level) { - const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent( - desc.Target, desc.Width, desc.Height, desc.Depth, level - 1); - const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent( - desc.Target, desc.Width, desc.Height, desc.Depth, level); - BlitColorTexture2D(textureId, static_cast(level - 1), 0, 0, - static_cast(srcSize.x()), static_cast(srcSize.y()), - textureId, static_cast(level), 0, 0, - static_cast(dstSize.x()), static_cast(dstSize.y()), filter); - } - return; - } -#endif + // P5e (tx2): the transport arm that stood here is GenerateColorTexture2DMipmapByRecord + // above (CONTRACT-P5E §4.4's site 9216); this overload is the monolith body. const Uint mipLevelCount = mipmapTexture->GetMipmapLevelCount(); MOBILEGL_ASSERT(mipLevelCount > 0, "Color mipmap generation requires allocated storage."); @@ -9560,6 +10039,65 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.2: glGenerateMipmap's body once the texture came from + // VerbMipRes. Same four arms as the frontend body, with every shape question answered from + // the descriptor. + // + // RULING 14, AND WHY THE ROW IS NOT FLIPPED HERE. The unit read this arm retires is what the + // wait existed for, but ONE frontend contact is left on the path and it is not tx2's to + // retire: the RGB16F/RGB32F CPU filter reaches into the client's level shadows + // (GenerateThreeChannelFloatMipmapOnCpu, kimi audit row 61) and P8/P9 own the pull. So the + // brief's escape applies - the arm is not complete, the row stays kWaitApplied and the flip + // is listed as trailing - and the record stays barriered, which is what keeps the CPU arm's + // refusal a refusal rather than a wrong picture. + static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes, + const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& backendTexture) { + const auto format = static_cast(record.Desc.InternalFormat); + const auto textureTarget = TextureTargetForPipeResourceTarget(record.Desc.Target); + + if (format == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(format) || + format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) { + // The storage verification EnsureGenerateMipmapStorageAllocated's disaggregated arm + // already makes, with the record passed in rather than re-read from VerbMipRes: the + // frontend defined the generated chain before emitting this verb, so the server only + // checks that the descriptor it was given can carry it. + EnsureGenerateMipmapStorageDescribed(record); + } + if (format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) { + // The CPU filter reads and writes the CLIENT's level shadows. It already aborts + // three frames deep at the MipmapStorage guard under a transport; naming it here + // says which emulation, which is what the guard cannot. + MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-cpu-filter"); + } + if (IsDepthOnlyFormat(format)) { + GenerateDepthTexture2DMipmapByRecord(record, backendTexture); + return; + } + if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) { + GenerateColorTexture2DMipmapByRecord(record, backendTexture); + return; + } + + const GLenum backendTarget = + TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target)); + // TempTextureUnit, not the active unit: the scratch bind is the backend's own and + // BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit + // was never anything but the frontend's way of naming the texture, and the record names + // it now. + backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit); + // ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the + // source texture is detached from every framebuffer that attaches it - by handle, over + // the applier's own framebuffer records. + ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes); + ScopedCompleteFramebufferBinding completeFramebuffer; + ClearGLErrors(); + g_GLESFuncs.glGenerateMipmap(backendTarget); + RecordGLError("glGenerateMipmap", backendTarget, format); + } +#endif + void PatchParameteri(GLenum pname, GLint value) { if (g_GLESFuncs.glPatchParameteri == nullptr) return; g_GLESFuncs.glPatchParameteri(pname, value); @@ -9568,6 +10106,32 @@ namespace MobileGL::MG_Backend::DirectGLES { void GenerateMipmap(GLenum target) { #if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG && MOBILEGL_ENABLE_SCOPE_MARKER DebugImpl::OpenGLScopeMarker marker(__func__); +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.2 / ruling 14 (ID-92's neighbour): GENERATE_MIPMAP RESOLVES + // ITS TEXTURE FROM VerbMipRes, NEVER FROM THE ACTIVE UNIT. + // + // The verb's own record names the texture (MGPMipPlan::Res, stashed as VerbMipRes) and + // EnsureGenerateMipmapStorageAllocated's disaggregated arm has read it since P5c. What + // stood here instead was GetActiveTextureUnit + GetTextureUnitObject + the target's + // binding slot - three BARRIER_PULLED reads to re-derive a handle the record carried - + // and they are the ONLY reason this row still waits: ClientSession.cpp parks the client + // so that the unit it reads is the unit the call was made on. With the unit read gone the + // wait has nothing left to protect, which is what lets PipeCalls.def's WaitClass column + // for GenerateMipmap move to kWaitNone. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes; + auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(mipRes); + const auto* record = PipeTextureRecordForHandle(mipRes); + if (!backendTexture || record == nullptr) { + MGLOG_E_ONCE("MGPipe: generate_mipmap names texture {%u, %u}, which has no applier " + "record or no driver texture; the call is dropped", + mipRes.Slot, mipRes.Gen); + return; + } + GenerateMipmapByRecord(target, mipRes, *record, backendTexture); + return; + } #endif auto unitIndex = MGB_CTX->GetActiveTextureUnit(); auto& unit = MGB_CTX->GetTextureUnitObject(unitIndex); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 9e7b021b..149bdd15 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -5874,6 +5874,59 @@ namespace MobileGL::MG_Backend::DirectGLES { return m_backendTextureId; } +#if MOBILEGL_PIPE_PUSH + // P5e (tx2), CONTRACT-P5E §5.2 / scout G-S2-5. See the declaration. + void BackendTextureObject::RequireImageBindableStorageByHandle( + MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record) { + if (m_imageBindableStorageRequired) { + return; + } +#if MOBILEGL_BUILD_DISAGGREGATED + // THE REFUSAL, AT THE ENTRY. The predicate is the SAME one the frontend overload + // hoisted to ahead of its first guarded contact - "would any level actually be + // replayed", answered from the server's own staged-texture store - so this changes + // WHERE the abort happens and not WHETHER it happens. What it buys is that the + // reason can still be stated: three frames deeper it was a MarkStorageDirty guard + // naming "texture-legacy-arm", which says nothing about image bindability. + // + // A texture with nothing defined yet is NOT refused: it is allocated image-bindable + // up front and pulls nothing, which is exactly what ImageBindableHint exists to make + // the common case. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + auto& store = MG_Remote::Server::ServerStagedTexture(); + const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res); + Bool anyLevelWouldReplay = false; + for (const auto& uploadTarget : + BufferImpl::StagedUploadTargetsForPipeTarget(record.Desc.Target)) { + for (Uint32 level = 0; level < record.Desc.Levels; ++level) { + if (store.IsLevelDefined(key, static_cast(uploadTarget), + static_cast(level))) { + anyLevelWouldReplay = true; + break; + } + } + if (anyLevelWouldReplay) break; + } + if (anyLevelWouldReplay) { + MG_Pipe::MGPipeUnmigratedEmulation("image-bindable-redirty"); + } + } +#else + (void)res; + (void)record; +#endif + // Nothing to replay, so the transition is just the two sticky flags plus the + // parameter resync the widening's swizzle needs - the whole of the frontend + // overload's first half, with its second half refused above. + m_imageBindableStorageRequired = true; + m_isInitialized = false; + // The widened carrier's swizzle override, exactly as the frontend overload's tail + // sets it and for the same reason: the parameter sync is gated on a params version + // this transition does not move. + m_forceTextureParamsResync = true; + } +#endif + void BackendTextureObject::RequireImageBindableStorage( const SharedPtr& stateTextureObject) { if (m_imageBindableStorageRequired) { @@ -6802,6 +6855,44 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } +#if MOBILEGL_PIPE_PUSH + // P5e (tx2): the GL name a log line wants, from whichever side of the arm can answer it. + // Desc.GlNameForDiag is the declared diagnostics carrier (MGPipeTypes.h) and it is the + // ONLY thing the by-handle arm may say about a texture's frontend name - a GL name is + // never an identity, never a memo key and never part of a hash (section 4.2.1). The + // twenty-odd GetExternalIndex() log reads this family carried (scout G-S2-4) all become + // this call. + static Uint32 TextureDiagName(const SharedPtr& stateTextureObject, + const MG_Pipe::MGPipeResourceRecord* record) { + if (stateTextureObject) return static_cast(stateTextureObject->GetExternalIndex()); + return record != nullptr ? record->Desc.GlNameForDiag : 0u; + } +// True while this twin has NO noted handle, i.e. it is the monolith-glue half and the frontend +// object below is the only thing that can answer. A macro and not a member function because the +// pull build has no such member at all and this has to fold to a constant there (D-P: the pull +// build's preprocessed text may not gain a call). +#define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (MG_Pipe::MGPipeHandleIsNull((twin).PushedSyncHandle())) +// P5e (tx2), CONTRACT-P5E §5.2: THE TEXTURE'S TARGET, from the descriptor whenever a record was +// resolved. The two live `GetTarget()` reads left INSIDE the handle arm (scout G-S2-2, the +// parameter sync and the built-in sampler sync) read Desc.Target here, through the same +// StagedTextureTargetForPipeTarget mapping the storage sync already routes through - so all +// three bodies answer the question from one carrier rather than two authorities. +#define MGB_TEXPARAM_TARGET(rec, obj) \ + ((rec) != nullptr ? BufferImpl::StagedTextureTargetForPipeTarget((rec)->Desc.Target) : (obj)->GetTarget()) +// The ~20 GetExternalIndex() reads this family makes in LOG LINES (scout G-S2-4). The pull +// expansion is the pre-P5e expression with one pair of parentheses around the receiver. +#define MGB_TEXTURE_DIAG_NAME(rec, obj) (TextureDiagName((obj), (rec))) +// The storage KIND, from the descriptor whenever one was resolved - the backstop arm of the +// storage switch used to ask the frontend object for a value it had just read from the record. +#define MGB_TEXTURE_STORAGE_KIND(rec, obj) \ + ((rec) != nullptr ? static_cast((rec)->Desc.StorageKind) : (obj)->GetStorageType()) +#else +#define MGB_TEXTURE_HANDLE_ARM_OFF(twin) (true) +#define MGB_TEXPARAM_TARGET(rec, obj) ((obj)->GetTarget()) +#define MGB_TEXTURE_DIAG_NAME(rec, obj) ((obj)->GetExternalIndex()) +#define MGB_TEXTURE_STORAGE_KIND(rec, obj) ((obj)->GetStorageType()) +#endif + // The ES entry point for EXT/OES_texture_view, whichever spelling this driver brought. // Callers must have checked g_GLESCapabilities.SupportsTextureView first - the capability // is the extension AND the pointer, because eglGetProcAddress hands back live-looking @@ -6918,6 +7009,60 @@ namespace MobileGL::MG_Backend::DirectGLES { StampViewSyncKeys(stateTextureObject); } +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2): see the declaration for the split between what the record answers and what + // the wire still cannot say about a view. + void BackendTextureObject::SyncTextureViewToBackendByRecord( + MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& stateTextureObject) { + if (!g_GLESCapabilities.SupportsTextureView) { + // Unreachable through the API - the frontend refuses glTextureView without + // GL_ARB_texture_view - and kept for the reason the frontend arm keeps its twin. + MGLOG_E_ONCE("Texture view %u reached the backend on a driver without " + "EXT/OES_texture_view.", + record.Desc.GlNameForDiag); + return; + } + // The storage owner by handle, through the same by-handle entry every other draw-path + // texture takes: no frontend object, no allocator probe. + const SharedPtr storageBackendObject = + SyncTextureToBackendByHandle(record.Desc.ViewOf, m_imageBindableStorageRequired); + if (!storageBackendObject) { + MGLOG_E_ONCE("Failed to sync the storage texture {%u, %u} of view %u on the handle arm.", + record.Desc.ViewOf.Slot, record.Desc.ViewOf.Gen, record.Desc.GlNameForDiag); + return; + } + const Uint storageBackendTextureId = storageBackendObject->GetBackendTextureId(); + if (storageBackendTextureId == 0) { + MGLOG_D("Storage texture of view %u has no ES name yet.", record.Desc.GlNameForDiag); + return; + } + if (m_isInitialized && m_viewSourceBackendTextureId == storageBackendTextureId) { + // The steady case, and the only one this arm has to be fast at: one record read, + // one integer compare and the serial stamp that keeps the aggregate clean gate + // (IsDrawSyncCleanByRecord) answering true for the next draw. + m_syncedResourceSerial = record.Serial; + return; + } + // TRAILING (tx2 report, "left barriered"): the glTextureView CALL needs the view + // window and MGPResourceDesc has no carrier for it. The frontend object is taken when + // the monolith-glue note still reaches one - which is every case a single-process + // split has - and the refusal is NAMED rather than silent for the case that does not. + if (stateTextureObject) { + SyncTextureViewToBackend(stateTextureObject); + m_syncedResourceSerial = record.Serial; + return; + } + MGLOG_E_ONCE("MGPipe: texture view {%u, %u} (GL %u) has to be (re-)created as a view of " + "{%u, %u}, and MGPResourceDesc carries no view window - minLevel, " + "numLevels, minLayer, numLayers have no wire field - so the by-handle arm " + "cannot issue glTextureView. P5e leaves view CREATION on the frontend arm; " + "this name will sample empty until it gets one.", + res.Slot, res.Gen, record.Desc.GlNameForDiag, record.Desc.ViewOf.Slot, + record.Desc.ViewOf.Gen); + } +#endif + // P4a (D-D5), the dirty-ownership inversion, applied at the two places the upload loops // below actually ask: "does this (uploadTarget, level) owe an upload" and "it has been // uploaded". ARCHITECTURE.md:253 puts the rect model and the emission cursor on the @@ -7109,11 +7254,59 @@ namespace MobileGL::MG_Backend::DirectGLES { void BackendTextureObject::SyncMipmapsToBackend( const SharedPtr& stateTextureObject) { - if (!stateTextureObject) { + // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; + // see the twin note in SyncBuiltinSamplerToBackend. + if (!stateTextureObject && MGB_TEXTURE_HANDLE_ARM_OFF(*this)) { MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); return; } +#if MOBILEGL_PIPE_PUSH + // P4a (D-B3/D-D5): on the handle arm the record IS the state this function reads. + // P5e (tx2) hoists the resolution to the TOP of the function, because the view test + // below needs it before anything else does; nothing else about it moves. + // + // The record pointer stays valid for the whole function: only a create/respecify for + // a NEW slot grows MGPipeApplierState::TextureResources, and nothing this function + // calls emits one. Pointers INTO record->PendingUploads do not - the consume is a + // swap-and-pop - so every one of them is taken, used and dropped inside one level's + // iteration, and never held across a consume. + const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr; + MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle; + if (TextureResourceSubsystemEnabled()) { + pushedStorage = ResolveOwnRecord(stateTextureObject); + pushedRes = !MG_Pipe::MGPipeHandleIsNull(m_pushedSyncHandle) + ? m_pushedSyncHandle + : (pushedStorage != nullptr ? pushedStorage->Desc.Resource + : MG_Pipe::kMGPipeNullHandle); + if (pushedStorage == nullptr) { + MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " + "storage and uploads cannot be driven from the pushed descriptor " + "(handle {%u, %u})", + TextureDiagName(stateTextureObject, pushedStorage), pushedRes.Slot, + pushedRes.Gen); + return; + } + } +#endif +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), CONTRACT-P5E §5.2 (scout G-S2-2): THE VIEW TEST MOVES BEHIND THE + // RECORD. `IsTextureView()` was the one frontend read this function made BEFORE it + // had resolved anything at all, and Desc.ViewOf is the carrier - it names the storage + // owner BY HANDLE, so both the question and its answer are server-owned. + // + // Ruling 1's arm: the push-monolith build keeps the frontend test below token for + // token, because there the two answers are the same answer and the verify comparator + // needs the frontend arm. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && pushedStorage != nullptr && + !MG_Pipe::MGPipeHandleIsNull(pushedStorage->Desc.ViewOf)) { + SyncTextureViewToBackendByRecord(pushedRes, *pushedStorage, stateTextureObject); + return; + } + if (MG_Config::Transport != MG_Config::TransportMode::Monolith && pushedStorage != nullptr) { + // Not a view: fall through to the storage body with the frontend test skipped. + } else +#endif // 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 @@ -7138,24 +7331,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // consume is a swap-and-pop - so every one of them is taken, used and dropped // inside one level's iteration, and never held across a consume. #if MOBILEGL_PIPE_PUSH - const MG_Pipe::MGPipeResourceRecord* pushedStorage = nullptr; - MG_Pipe::MGPipeHandle pushedRes = MG_Pipe::kMGPipeNullHandle; - if (TextureResourceSubsystemEnabled()) { -#if MOBILEGL_BUILD_DISAGGREGATED - // P5c (G6, CONTRACT-P5C §5.4): the sync arrives holding the frontend object - // (the object-class rows) and resolves its handle by frontend identity - - // named debt inside the scope, P3b/P4b rekeys the registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; -#endif - pushedRes = g_backendTextureObjects.HandleOf(stateTextureObject.get()); - pushedStorage = PipeTextureRecordForHandle(pushedRes); - if (pushedStorage == nullptr) { - MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " - "storage and uploads cannot be driven from the pushed descriptor " - "(handle {%u, %u})", - stateTextureObject->GetExternalIndex(), pushedRes.Slot, pushedRes.Gen); - return; - } + if (pushedStorage != nullptr) { // THE METADATA RESPECIFY, HONOURED (ID-18 M4, review M-2). BindMask and // ImageBindableHint are STICKY facts the client discovers AFTER allocation - a // texture first bound as a shader image - and an IMMUTABLE texture never has a @@ -7179,6 +7355,14 @@ namespace MobileGL::MG_Backend::DirectGLES { pushedStorage->Desc.ImageBindableHint != 0 || (pushedStorage->Desc.BindMask & MG_Pipe::kMGPipeBindShaderImage) != 0; if (pushedWantsImageBindableStorage && !m_imageBindableStorageRequired) { +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2), ruling 1's arm: under a transport the re-dirty half is a NAMED + // refusal at its entry (§5.2) instead of a walk of the client's level shadows; + // the push-monolith build keeps the frontend transition exactly as it is. + if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { + RequireImageBindableStorageByHandle(pushedRes, *pushedStorage); + } else +#endif RequireImageBindableStorage(stateTextureObject); } @@ -7225,7 +7409,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } MGLOG_D("Syncing texture mipmaps with backend ID %u to backend for state ID %u", m_backendTextureId, - stateTextureObject->GetExternalIndex()); + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); GLenum target = ConvertTextureTargetToBackendGLEnum(MGB_TEXTURE_TARGET(stateTextureObject)); auto targetInternal = MGB_TEXTURE_TARGET(stateTextureObject); @@ -7286,7 +7470,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } if (!anyDefined) { MGLOG_D("Texture object with ID: %u has no defined image level, skipping sync.", - stateTextureObject->GetExternalIndex()); + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); return; } } else @@ -7916,7 +8100,7 @@ namespace MobileGL::MG_Backend::DirectGLES { "different level pitches (%u/%u and %u/%u) - a pitch is " "the LEVEL's, so the carried pair is dropped and the " "extent is used instead", - stateTextureObject->GetExternalIndex(), + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), static_cast(level), firstRegion.SrcRowStride, firstRegion.SrcSliceStride, region.SrcRowStride, region.SrcSliceStride); @@ -7964,7 +8148,7 @@ namespace MobileGL::MG_Backend::DirectGLES { "%llu is not its own origin (%d,%d,%d) at the level's pitch " "(%llu expected) - the carried offsets are dropped and every " "rect is read from the shadow instead", - stateTextureObject->GetExternalIndex(), static_cast(level), + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), static_cast(level), static_cast(region.SrcOffset), region.X, region.Y, region.Z, static_cast(derivedOffset)); @@ -8283,32 +8467,48 @@ namespace MobileGL::MG_Backend::DirectGLES { case TextureStorageType::Buffer: { auto* textureBufferObject = static_cast(stateTextureObject.get()); - auto& slot = textureBufferObject->GetBufferBindingSlot(); - auto& buffer = slot.GetBoundObject(); - if (!buffer) { - MGLOG_D("Texture buffer object with ID: %u has no bound buffer, skipping sync.", - stateTextureObject->GetExternalIndex()); - return; - } #if MOBILEGL_PIPE_PUSH - // M-2: THE BUFFER-TEXTURE FIELDS ARE THE DESCRIPTOR'S TOO - BufferForTexBuffer - // names the backing store and BufOffset/BufSize name the window. The frontend - // binding slot above is still what hands over the BufferObject, because - // EnsureBufferResourceForHandle takes it as the one question P3a's record cannot - // answer (an emulated persistent map, D-A4's `frontend` argument) - but WHICH - // buffer, and which window of it, are read from the record. + // P5e (tx2), scout R5 RESOLVED. THE BACKING BUFFER IS THE DESCRIPTOR'S, AND SO IS + // ITS WINDOW - `Desc.BufferForTexBuffer` names the store and `Desc.BufOffset / + // BufSize` name the range, so the frontend binding slot below is not read at all + // on the handle arm. + // + // The one thing that argument had to answer is D-A4's `frontend` parameter of + // EnsureBufferResourceForHandle, which existed for the emulated-persistent-map + // question. The E note's reading is confirmed at this head: that function's three + // frontend reads (Managers.cpp's MappedData / SyncPersistentMappedRange / + // HasDefinedContent) are all inside its MONOLITH arm - under an active transport + // it is client-free (kimi audit row 29) - so `nullptr` is not a hole, it is the + // whole of the split answer, and it is what §4.2's Buffer row already prescribes + // (`EnsureBufferResourceForHandle(nullptr, h)`). + // + // WHOLE-VS-RANGE likewise stops asking the buffer object for its size: the + // sentinel kMGPipeWholeBuffer IS "the whole store", which is the question + // `rangeSize == buffer->GetSize()` was spelling the long way round. MG_Pipe::MGPipeHandle pushedTexBuffer = MG_Pipe::kMGPipeNullHandle; if (pushedStorage != nullptr) { pushedTexBuffer = pushedStorage->Desc.BufferForTexBuffer; if (MG_Pipe::MGPipeHandleIsNull(pushedTexBuffer)) { MGLOG_E_ONCE("MGPipe: buffer texture %u's descriptor names no backing buffer on " "the handle arm, so it cannot be backed (handle {%u, %u})", - stateTextureObject->GetExternalIndex(), pushedRes.Slot, pushedRes.Gen); + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), pushedRes.Slot, pushedRes.Gen); return; } } + const Bool texBufferByRecord = + pushedStorage != nullptr && MGB_TEXTURE_HANDLE_ARM_OFF(*this) == false; +#else + const Bool texBufferByRecord = false; #endif - auto bufferIndex = buffer->GetExternalIndex(); + static const SharedPtr kNoFrontendBuffer{}; + auto& buffer = texBufferByRecord ? kNoFrontendBuffer + : textureBufferObject->GetBufferBindingSlot().GetBoundObject(); + if (!buffer && !texBufferByRecord) { + MGLOG_D("Texture buffer object with ID: %u has no bound buffer, skipping sync.", + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); + return; + } + auto bufferIndex = buffer ? buffer->GetExternalIndex() : 0u; #if MOBILEGL_PIPE_PUSH // The memo key is the HANDLE's slot on the handle arm: a GL name is never an // identity (section 4.2.1), and the record's Serial covers a slot recycled at a @@ -8331,7 +8531,7 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif if (!backendBufferResource || backendBufferResource->id == 0) { MGLOG_E_ONCE("Failed to sync backing buffer for texture buffer with ID: %u", - stateTextureObject->GetExternalIndex()); + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); return; } @@ -8379,13 +8579,13 @@ namespace MobileGL::MG_Backend::DirectGLES { "shader declaring a samplerBuffer will fail to compile. MobileGL " "still advertises GL_MAX_TEXTURE_BUFFER_SIZE = %d because an " "OpenGL 4.x context may not report 0.", - stateTextureObject->GetExternalIndex(), GetBufferTextureTierName(), + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject), GetBufferTextureTierName(), g_GLESCapabilities.MaxTextureBufferSize); break; } MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with " "ID: %u, buffer ID: %u, buffer size: %zu, format: %s", - m_backendTextureId, backendId, buffer->GetSize(), + m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize, MG_Util::ConvertGLEnumToString(glInternalFormat).c_str()); // A texture that names a window of the buffer needs the range form; the // whole-buffer forms report offset 0 and the buffer's current size, which @@ -8400,13 +8600,24 @@ namespace MobileGL::MG_Backend::DirectGLES { const SizeT rangeOffset = pushedStorage != nullptr ? static_cast(pushedStorage->Desc.BufOffset) : textureBufferObject->GetBufferRangeOffset(); + // P5e (tx2): "the whole store" is the SENTINEL on the handle arm, not a size + // compare against a frontend BufferObject this arm may not name. The monolith + // arm keeps resolving the sentinel live, exactly as its comment says. + const Bool wholeBufferRange = + pushedStorage != nullptr + ? (pushedStorage->Desc.BufOffset == 0 && + pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer) + : (textureBufferObject->GetBufferRangeOffset() == 0 && + textureBufferObject->GetBufferRangeSizeInBytes() == buffer->GetSize()); const SizeT rangeSize = pushedStorage == nullptr ? textureBufferObject->GetBufferRangeSizeInBytes() : (pushedStorage->Desc.BufSize == MG_Pipe::kMGPipeWholeBuffer - ? buffer->GetSize() + ? (buffer ? buffer->GetSize() : 0u) : static_cast(pushedStorage->Desc.BufSize)); #else + const Bool wholeBufferRange = textureBufferObject->GetBufferRangeOffset() == 0 && + textureBufferObject->GetBufferRangeSizeInBytes() == buffer->GetSize(); const SizeT rangeOffset = textureBufferObject->GetBufferRangeOffset(); const SizeT rangeSize = textureBufferObject->GetBufferRangeSizeInBytes(); #endif @@ -8414,14 +8625,14 @@ namespace MobileGL::MG_Backend::DirectGLES { // the unsuffixed entry points are the ES 3.2 core spelling, and a driver // whose buffer textures come from EXT/OES_texture_buffer exports the // suffixed ones instead. The dispatchers pick whichever this tier ships. - if (rangeOffset == 0 && rangeSize == buffer->GetSize()) { + if (wholeBufferRange) { CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId); } else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, glInternalFormat, backendId, static_cast(rangeOffset), static_cast(rangeSize))) { MGLOG_W_ONCE("Texture buffer %u names a sub-range but the driver has no " "glTexBufferRange; binding the whole buffer instead", - stateTextureObject->GetExternalIndex()); + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); CallTexBuffer(GL_TEXTURE_BUFFER, glInternalFormat, backendId); } DebugImpl::ErrorLopper::Loop( @@ -8443,10 +8654,10 @@ namespace MobileGL::MG_Backend::DirectGLES { if (m_bufferImageSplitViewId == 0) { MGLOG_E_ONCE("Failed to generate the buffer-image split view for texture %u; " "its image binding will read the unsplit view.", - stateTextureObject->GetExternalIndex()); + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); } else { g_GLESFuncs.glBindTexture(GL_TEXTURE_BUFFER, m_bufferImageSplitViewId); - if (rangeOffset == 0 && rangeSize == buffer->GetSize()) { + if (wholeBufferRange) { CallTexBuffer(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId); } else if (!CallTexBufferRange(GL_TEXTURE_BUFFER, bufferImageSplitFormat, backendId, static_cast(rangeOffset), @@ -8470,8 +8681,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // renders wrong; throwing unwinds through the C GL ABI and kills the process. MGLOG_E_ONCE("DirectGLES texture sync: no upload path for storage type %d on texture %u; " "skipping this sync", - static_cast(stateTextureObject->GetStorageType()), - stateTextureObject->GetExternalIndex()); + static_cast(MGB_TEXTURE_STORAGE_KIND(pushedStorage, stateTextureObject)), + MGB_TEXTURE_DIAG_NAME(pushedStorage, stateTextureObject)); break; } @@ -8530,23 +8741,34 @@ namespace MobileGL::MG_Backend::DirectGLES { #endif #if MOBILEGL_PIPE_PUSH - const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler( - const SharedPtr& stateTextureObject) { - // MONOLITH GLUE, named as such (HandleOfBuffer's shape): the Texture handle of an - // object this backend still arrives holding. Under a real split the handle rides in - // the payload; the client mints it off this object's lifetime id, so the allocator - // resolves it here through the table's own single-entry front memo. + // 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. + 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; #if MOBILEGL_BUILD_DISAGGREGATED - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the - // scope - P3b/P4b rekeys the registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; + // 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; #endif - const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get()); - const auto* record = PipeTextureRecordForHandle(res); + res = g_backendTextureObjects.HandleOf(stateTextureObject.get()); + } + return PipeTextureRecordForHandle(res); + } + + const SamplerParameters* BackendTextureObject::ResolvePushedBuiltinSampler( + const SharedPtr& stateTextureObject, + const MG_Pipe::MGPipeResourceRecord* record) { if (record == nullptr) { MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " "built-in sampler cannot be pushed (handle {%u, %u})", - stateTextureObject->GetExternalIndex(), res.Slot, res.Gen); + TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot, + m_pushedSyncHandle.Gen); return nullptr; } // NO set_texture_params HAS BEEN APPLIED FOR THIS TEXTURE YET, which is not the same @@ -8563,7 +8785,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (record->ParamsSerial == 0) { MGLOG_D("Texture %u has no set_texture_params record yet, so it has no built-in " "sampler to push.", - stateTextureObject->GetExternalIndex()); + TextureDiagName(stateTextureObject, record)); return nullptr; } // D-E1: kMGPipeNullHandle in a record that WAS written is Fatal{ProtocolCorruption} @@ -8576,14 +8798,14 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_E_ONCE("MGPipe: texture %u's set_texture_params names the null handle as its " "built-in sampler CSO; every texture owns a sampler object, so this " "record is malformed - declining the built-in sampler push", - stateTextureObject->GetExternalIndex()); + TextureDiagName(stateTextureObject, record)); return nullptr; } const auto* cso = PipeSamplerCsoRecordForHandle(builtin); if (cso == nullptr) { MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier " "record - declining the built-in sampler push", - stateTextureObject->GetExternalIndex(), builtin.Slot, builtin.Gen); + TextureDiagName(stateTextureObject, record), builtin.Slot, builtin.Gen); return nullptr; } // THE HANDLE IS PART OF THE KEY, not just the serial. Sampler CSOs are @@ -8618,7 +8840,11 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - if (!stateTextureObject) { + // P5e (tx2): NULL IS THE BY-HANDLE ARM, not an error, once a handle has been noted on + // this twin - the record and the built-in sampler's CSO answer everything below and + // the frontend object is not consulted at all. Without a handle it is still the + // caller's bug it always was. + if (!stateTextureObject && MGB_TEXTURE_HANDLE_ARM_OFF(*this)) { MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); return; } @@ -8635,8 +8861,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // measured at -2 bytes on this function, which is a G1 failure. #if MOBILEGL_PIPE_PUSH const SamplerParameters* pendingSamplerParams = nullptr; + // P5e (tx2): resolved ONCE and shared with the target read below, which used to be + // the frontend `GetTarget()` this arm was not allowed to make. + const MG_Pipe::MGPipeResourceRecord* pushedDesc = nullptr; if (TextureResourceSubsystemEnabled()) { - pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject); + pushedDesc = ResolveOwnRecord(stateTextureObject); + pendingSamplerParams = ResolvePushedBuiltinSampler(stateTextureObject, pushedDesc); if (pendingSamplerParams == nullptr) return; } else { #if !MOBILEGL_PIPE_LEGACY_MEMOS @@ -8675,11 +8905,18 @@ namespace MobileGL::MG_Backend::DirectGLES { m_forceSamplerResync = false; #endif +#if MOBILEGL_PIPE_PUSH + MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u", + m_backendTextureId, TextureDiagName(stateTextureObject, pushedDesc)); + auto targetInternal = MGB_TEXPARAM_TARGET(pushedDesc, stateTextureObject); + GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal); +#else MGLOG_D("Syncing texture built-in sampler with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); +#endif MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); if (!IsSupportedTextureTarget(targetInternal)) { @@ -8772,17 +9009,14 @@ namespace MobileGL::MG_Backend::DirectGLES { #if MOBILEGL_PIPE_PUSH const MG_Pipe::MGPipeResourceRecord* BackendTextureObject::ResolvePushedTextureParams( const SharedPtr& stateTextureObject) { -#if MOBILEGL_BUILD_DISAGGREGATED - // P5c (G6, CONTRACT-P5C §5.4): frontend-keyed resolution, named debt inside the - // scope - P3b/P4b rekeys the registry onto handles. - const MG_Pipe::MGPipeFrontendKeyedRegistryScope frontendKeyedRegistry; -#endif - const MG_Pipe::MGPipeHandle res = g_backendTextureObjects.HandleOf(stateTextureObject.get()); - const auto* record = PipeTextureRecordForHandle(res); + // P5e (tx2): the handle the caller adopted this twin with, and only failing that the + // client allocator - ResolveOwnRecord holds the whole of that rule. + const auto* record = ResolveOwnRecord(stateTextureObject); if (record == nullptr) { MGLOG_E_ONCE("MGPipe: texture %u has no applier record on the handle arm, so its " "parameters cannot be pushed (handle {%u, %u})", - stateTextureObject->GetExternalIndex(), res.Slot, res.Gen); + TextureDiagName(stateTextureObject, record), m_pushedSyncHandle.Slot, + m_pushedSyncHandle.Gen); return nullptr; } // ParamsSerial + ForceResync replace m_syncedTextureParamsVersion + @@ -8866,7 +9100,9 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - if (!stateTextureObject) { + // P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin; + // see the twin note in SyncBuiltinSamplerToBackend. + if (!stateTextureObject && MGB_TEXTURE_HANDLE_ARM_OFF(*this)) { MGLOG_E_ONCE("State texture object is null, cannot sync to backend."); return; } @@ -8899,7 +9135,7 @@ namespace MobileGL::MG_Backend::DirectGLES { borderColorReadable = false; MGLOG_E_ONCE("MGPipe: texture %u's built-in sampler CSO {%u, %u} has no applier " "record, so its border colour cannot be pushed", - stateTextureObject->GetExternalIndex(), + TextureDiagName(stateTextureObject, pushedRecord), pushedRecord->Params.BuiltinSampler.Slot, pushedRecord->Params.BuiltinSampler.Gen); } @@ -8931,11 +9167,19 @@ namespace MobileGL::MG_Backend::DirectGLES { m_forceTextureParamsResync = false; #endif +#if MOBILEGL_PIPE_PUSH + MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId, + TextureDiagName(stateTextureObject, pushedRecord)); + + auto targetInternal = MGB_TEXPARAM_TARGET(pushedRecord, stateTextureObject); + GLenum target = ConvertTextureTargetToBackendGLEnum(targetInternal); +#else MGLOG_D("Syncing texture params with backend ID %u to backend for state ID %u", m_backendTextureId, stateTextureObject->GetExternalIndex()); GLenum target = ConvertTextureTargetToBackendGLEnum(stateTextureObject->GetTarget()); auto targetInternal = stateTextureObject->GetTarget(); +#endif MGLOG_D(" Texture target for syncing is %s", MG_Util::ConvertTextureTargetToString(targetInternal).c_str()); if (!IsSupportedTextureTarget(targetInternal)) { @@ -13838,16 +14082,122 @@ namespace MobileGL::MG_Backend::DirectGLES { } // namespace namespace TextureImpl { + // ---- P5e (tx2): THE TEXTURE FAMILY'S BY-HANDLE ENTRY, CONTRACT-P5E §5.2 -------------- + // + // SamplerImpl::ResolveSamplerCsoTwin is the shape (Managers.cpp, the sampler half was + // already run-ahead clean on its record arm and this is that shape brought to the texture + // half): RECORD FIRST, twin by GetOrCreateByHandle, three syncs driven from the record, + // no frontend touch and no allocator probe anywhere on the path. + // + // WHAT THE FRONTEND ENTRY PAYS AND THIS ONE DOES NOT. SyncTextureObjectToBackend takes a + // by-VALUE copy of the twin and does a second Find at the tail, because its registry's + // Find returns a reference INTO an open-addressed map that a NESTED sync (a view syncing + // its storage texture) can rehash out from under it. A slot-table entry is an ARRAY + // ELEMENT: a nested GetOrCreateByHandle can only grow the vector, so the answer is + // re-INDEXED at the tail rather than pinned by a refcount, and the copy is a refcount + // this arm does not pay. + // + // A null return is a NAMED decline and never a mint: no record (the client named a + // texture it never created, or one whose resource_destroy has already applied), or a slot + // whose live generation is ahead of the handle's. A caller binds nothing and the unit + // keeps what it holds. SharedPtr& SyncTextureToBackendByHandle(MG_Pipe::MGPipeHandle texture, Bool imageBindableStorageRequired) { - (void)imageBindableStorageRequired; - MGPipeP5eSeamNotLanded("TextureImpl::SyncTextureToBackendByHandle", "tx2", texture); +#ifdef TRACY_ENABLE + ZoneScopedC(TRACY_ZONECOLOR_BACKEND); +#endif + // The one storage every decline arm can return a reference to. Never written. + static SharedPtr s_noTwin{}; + s_noTwin = nullptr; + + const auto* record = PipeTextureRecordForHandle(texture); + if (record == nullptr) { + MGLOG_E_ONCE("MGPipe: texture {%u, %u} has no applier resource record on the handle " + "arm, so no driver texture can be built for it and the caller keeps " + "what it holds", + texture.Slot, texture.Gen); + return s_noTwin; + } + auto* slot = g_backendTextureObjects.GetOrCreateByHandle(texture); + if (slot == nullptr) { + MGLOG_E_ONCE("MGPipe: texture {%u, %u} cannot be adopted on the handle arm (the slot's " + "live generation is %u), so no driver texture is built for it", + texture.Slot, texture.Gen, g_backendTextureObjects.LiveGenAt(texture.Slot)); + return s_noTwin; + } + if (!*slot) *slot = MakeShared(); + // THE HANDLE IS NOTED BEFORE ANY SYNC RUNS. It is what the three prologues read + // instead of probing the client allocator, and what selects the by-handle arm inside + // them, so it has to be in place before the first of them is called. + (*slot)->NotePushedSyncHandle(texture); + + const SharedPtr twin = *slot; + if (imageBindableStorageRequired) { + twin->RequireImageBindableStorageByHandle(texture, *record); + } + static const SharedPtr kNoFrontendTexture{}; + twin->SyncTextureParamsToBackend(kNoFrontendTexture); + twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture); + twin->SyncMipmapsToBackend(kNoFrontendTexture); + // The storage sync may RE-MINT the driver texture, which discards every parameter the + // two calls above just pushed - the frontend entry's argument, unchanged. + if (twin->NeedsParameterResync()) { + twin->SyncTextureParamsToBackend(kNoFrontendTexture); + twin->SyncBuiltinSamplerToBackend(kNoFrontendTexture); + } + // ONE re-index, and only because a nested adopt may have GROWN the vector; nothing on + // this arm erases a live slot, so the entry itself cannot have gone. + auto* refreshed = g_backendTextureObjects.FindByHandle(texture); + if (refreshed != nullptr && *refreshed) { + return *refreshed; + } + s_noTwin = twin; + return s_noTwin; } void BackendTextureObject::SyncMipmapsToBackendByHandle(MG_Pipe::MGPipeHandle texture) { - MGPipeP5eSeamNotLanded("BackendTextureObject::SyncMipmapsToBackendByHandle", "tx2", - texture); + // The STORAGE half alone, for fb's attachment sync and the image sweep: an attachment + // needs its levels on the driver, not its sampler parameters (§5.4). The handle note + // is what routes the body below onto the record. + NotePushedSyncHandle(texture); + static const SharedPtr kNoFrontendTexture{}; + SyncMipmapsToBackend(kNoFrontendTexture); } + +#if MOBILEGL_PIPE_PUSH + // P5e (tx2), CONTRACT-P5E §5.2. See the declaration for the clause-by-clause derivation + // and for why BOTH halves of each resync pair are read here. + Bool BackendTextureObject::IsDrawSyncCleanByRecord(MG_Pipe::MGPipeHandle res, + const MG_Pipe::MGPipeResourceRecord& record) const { + (void)res; + // SyncMipmapsToBackend's own early-out, verbatim. + if (!m_isInitialized || m_syncedResourceSerial == 0 || m_syncedResourceSerial != record.Serial || + !record.PendingUploads.empty()) { + return false; + } + // SyncTextureParamsToBackend's (ResolvePushedTextureParams' gate). + if (m_syncedParamsSerial != record.ParamsSerial || record.Params.ForceResync != 0 || + m_forceTextureParamsResync) { + return false; + } + // SyncBuiltinSamplerToBackend's (ResolvePushedBuiltinSampler's gate). A texture whose + // params have never been set has no built-in sampler to push and that prologue + // declines SILENTLY - so a zero ParamsSerial is clean here for the same reason, and + // the CSO lookup below is only made when there is a CSO to look up. + if (record.ParamsSerial != 0) { + const MG_Pipe::MGPipeHandle builtin = record.Params.BuiltinSampler; + if (m_syncedBuiltinSampler != builtin) return false; + const auto* cso = PipeSamplerCsoRecordForHandle(builtin); + if (cso == nullptr || m_syncedBuiltinSamplerSerial != cso->Serial) return false; + if (record.Params.SamplerResync != 0 || m_forceSamplerResync) return false; + } else if (m_forceSamplerResync) { + return false; + } + // The storage-kind restriction the frontend gate carries, from the descriptor: a + // buffer texture's backing store can move without any serial above noticing. + return static_cast(record.Desc.StorageKind) == TextureStorageType::Mipmap; + } +#endif } // namespace TextureImpl namespace FramebufferImpl { diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index f0f426d8..0a50b656 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1641,6 +1641,26 @@ namespace MobileGL::MG_Backend::DirectGLES { // parameter and sampler halves are unchanged and run on this name as on any other. void SyncTextureViewToBackend(const SharedPtr& stateTextureObject); void StampViewSyncKeys(const SharedPtr& stateTextureObject); +#if MOBILEGL_BUILD_DISAGGREGATED + // P5e (tx2). THE VIEW ARM UNDER A TRANSPORT, and what it can and cannot answer. + // + // `Desc.ViewOf` names the storage owner BY HANDLE, so the STEADY question - "is my ES + // name still a view of the same storage name" - is answered entirely server-side: sync + // the storage twin by handle, compare its id against m_viewSourceBackendTextureId, + // stamp the record's Serial. That is the per-draw cost of every already-synced view + // and it reads nothing from the client. + // + // The CREATION of the view is the gap and it is named rather than papered over: the + // glTextureView call needs (minLevel, numLevels, minLayer, numLayers) and MGPResourceDesc + // carries none of the four - `ViewOf` is the only view field on the wire. So a view whose + // ES name has not been made a view YET, or whose storage was re-minted underneath it, + // still needs the frontend object and takes it when one is reachable; with no object it + // declines LOUDLY and the view samples its own (empty) name. Listed as trailing in the + // tx2 report with the wire fields it needs. + void SyncTextureViewToBackendByRecord( + MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPipeResourceRecord& record, + const SharedPtr& stateTextureObject); +#endif // The storage half of the sync for a texture created by glTextureView. Instead of // allocating storage and replaying uploads, it makes this object's ES name BE a view // of the storage texture's ES name (EXT/OES_texture_view), which is what gives the @@ -1654,6 +1674,24 @@ namespace MobileGL::MG_Backend::DirectGLES { // re-mint allocates fresh storage and only replays what the shadow still calls dirty. void RequireImageBindableStorage( const SharedPtr& stateTextureObject); +#if MOBILEGL_PIPE_PUSH + // P5e (tx2), CONTRACT-P5E §5.2 / G-S2-5. THE SAME TRANSITION WITH NO FRONTEND + // ARGUMENT, and with the re-dirty NAMED AT THE ENTRY instead of three frames deep. + // + // The frontend overload's whole second half is the re-dirty: it walks the CLIENT's + // level shadows to re-arm every level the widened carrier owes. A server has no + // client address space to walk, so under a transport that half is not "not migrated + // yet", it is not expressible - ImageBindableHint is the prevention (a texture that + // has ever been image-bound is allocated in the carrier from the start) and P9 owns + // the pull. The refusal is raised here, at the entry, where the reason can still be + // stated, rather than at the MarkStorageDirty guard the loop would reach. + // + // A texture arriving here with NO backend storage yet pulls nothing - it is simply + // allocated image-bindable up front - so that case is not refused, it is the whole + // point of the hint. + void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res, + const MG_Pipe::MGPipeResourceRecord& record); +#endif // Whether this texture's ES storage was minted in an image carrier rather than in the // frontend format's own layout - the readback has to ask, because for a NORMALIZED // carrier the storage is an integer texture holding codes and glGetTexImage still owes @@ -1702,6 +1740,39 @@ namespace MobileGL::MG_Backend::DirectGLES { return t->GetStorageType() == TextureStorageType::Mipmap; } +#if MOBILEGL_PIPE_PUSH + // P5e (tx2), CONTRACT-P5E §5.2. THE SAME AGGREGATE GATE WITH NO FRONTEND OBJECT IN + // IT, and it is EXACTLY the conjunction of the three handle-arm prologues' own + // early-outs - the relation IsDrawSyncClean states above for the frontend versions, + // restated over the server's serials: + // + // SyncMipmapsToBackend m_isInitialized && m_syncedResourceSerial == Serial + // && PendingUploads.empty() + // SyncTextureParamsToBackend m_syncedParamsSerial == ParamsSerial + // && !Params.ForceResync && !m_forceTextureParamsResync + // SyncBuiltinSamplerToBackend m_syncedBuiltinSampler == Params.BuiltinSampler + // && m_syncedBuiltinSamplerSerial == that CSO's Serial + // && !Params.SamplerResync && !m_forceSamplerResync + // + // plus the storage-kind restriction the frontend gate carries, answered from + // Desc.StorageKind. No new member and no new wire field: every input already + // exists, which is the whole reason the memo-HIT path can stop being frontend-bound. + // + // BOTH SIDES OF THE RESYNC PAIR ARE READ AND NEITHER CLEARS THE OTHER'S (D-E2, now + // load-bearing for a GATE rather than only for a push, scout R4). `Params.ForceResync` + // and `Params.SamplerResync` are the CLIENT's bits: the server reads them, acts on + // them and never writes them back. `m_forceTextureParamsResync`/`m_forceSamplerResync` + // are the SERVER's: set by RecreateBackendTexture / RequireImageBindableStorage and + // cleared only by the prologue that consumed them. A gate that read one side would + // skip the sync the other side is asking for, which for the sampler half is not + // mis-filtering but an INCOMPLETE texture sampling (0,0,0,1). + // + // The contextId / samplingGeneration arguments are GONE: the applier's ContextSerial + // and TextureShutterSerial are what the caller's keys already carry. + Bool IsDrawSyncCleanByRecord(MG_Pipe::MGPipeHandle res, + const MG_Pipe::MGPipeResourceRecord& record) const; +#endif + private: void RecreateBackendTexture(); @@ -1811,8 +1882,17 @@ namespace MobileGL::MG_Backend::DirectGLES { // over, and quietly re-reading the object would hide a missing record behind a // picture that still looks right - which is precisely what the subsystem A/B exists // to expose (MarkBufferGpuWritten's note, P3a). + // + // P5e (tx2): the HANDLE comes from m_pushedSyncHandle when the caller adopted this + // twin by handle, and only then from the client allocator - which is what retires + // the two `HandleOf` probes this pair used to make on every feature path. The record + // is resolved once by ResolveOwnRecord below and passed to the sampler half rather + // than looked up twice. + const MG_Pipe::MGPipeResourceRecord* ResolveOwnRecord( + const SharedPtr& stateTextureObject) const; const SamplerParameters* ResolvePushedBuiltinSampler( - const SharedPtr& stateTextureObject); + const SharedPtr& stateTextureObject, + const MG_Pipe::MGPipeResourceRecord* record); // Hands back the whole RECORD rather than its Params, because the parameter push // reads two things from beside them: Desc.InternalFormat, which decides the two // channel-widening swizzle compositions, and Params.BuiltinSampler, which is where @@ -1854,6 +1934,27 @@ namespace MobileGL::MG_Backend::DirectGLES { // (two textures sharing one CSO, then one of them diverging). Uint64 m_syncedBuiltinSamplerSerial = 0; MG_Pipe::MGPipeHandle m_syncedBuiltinSampler = MG_Pipe::kMGPipeNullHandle; + + // P5e (tx2), CONTRACT-P5E §4.1's `m_handle` (the m_pushedSyncHandle pattern the + // framebuffer twin already carries). THE HANDLE THIS TWIN WAS ADOPTED FOR. + // + // It is what replaces `g_backendTextureObjects.HandleOf(stateTextureObject.get())` + // in the three sync prologues: the by-handle entry point knows the handle before it + // knows anything else, so the prologues stop probing the client allocator to + // re-derive an answer the caller already had. It is ALSO the arm selector - a twin + // with a handle noted syncs from the record and tolerates a null frontend object, + // one without it is the monolith-glue half and reads the object as it always did. + // + // Stamped by SyncTextureToBackendByHandle / ResolveTextureTwin and never cleared: + // a slot recycled to {s, g+1} RESETS the twin (SlotTables.h's forward-Gen rule), so + // a stale handle cannot outlive the object it names. + MG_Pipe::MGPipeHandle m_pushedSyncHandle = MG_Pipe::kMGPipeNullHandle; + + public: + void NotePushedSyncHandle(MG_Pipe::MGPipeHandle res) { m_pushedSyncHandle = res; } + MG_Pipe::MGPipeHandle PushedSyncHandle() const { return m_pushedSyncHandle; } + + private: #endif }; diff --git a/MobileGL/MG_Remote/Server/PipeApplier.cpp b/MobileGL/MG_Remote/Server/PipeApplier.cpp index cfd3f735..34dc02cd 100644 --- a/MobileGL/MG_Remote/Server/PipeApplier.cpp +++ b/MobileGL/MG_Remote/Server/PipeApplier.cpp @@ -427,6 +427,66 @@ namespace MobileGL::MG_Remote::Server { // with a span (the client refuses "MultiDrawElements+CLIENT_INDICES" first; P8's // HostResolve.cpp flattens it) and a multi-draw that claims instancing (no GL entry point // produces one; the client never sends it). +#if MOBILEGL_BUILD_DISAGGREGATED + namespace { + // P5e (tx2), CONTRACT-P5E §5.3 / ruling 19 (ID-95, A8 closed). THE TWO UNIT WINDOWS MUST + // COVER [0, MaxTouchedTextureUnit], AND THIS IS WHERE THAT PROMISE IS CHECKED. + // + // Everything the texture and sampler families do per draw now reads + // [SamplerViewStart, +SamplerViewCount) and [SamplerStateStart, +SamplerStateCount). + // Under run-ahead a window NARROWER than the frontend's high-water mark silently drops a + // sync of a texture the draw is about to sample, or leaves an earlier draw's sampler + // object on a unit - and a decline is exactly what run-ahead cannot take, because the + // client has already moved on and there is no wait in which to notice. + // + // A SERVER-SIDE RE-DERIVATION IS REFUSED, and that is the ruling's point: the client owns + // the high-water mark (it is the `count` argument SamplerEmit.h passes, Start=0 / + // Count=maxTouched+1) and the server's job is to check the promise, not to invent a + // second authority for it. So this is a CHECK and its failure is corruption. + // + // WHERE THE MARK COMES FROM: MGPContextValues::MaxTouchedTextureUnit, applied by + // set_context_values, which precedes the verb on the ring - the same ordering §2.1's + // XFB clause leans on. It is RECORD_SUPPLIED, so reading it is reading what a record + // put there and not client memory (rule F). + // + // WIDER IS FINE. A window larger than the mark costs a walk over provably-empty units; + // only SMALLER is unrepresentable. An empty window on a draw that touches no unit at all + // (mark 0 with nothing ever touched) is the correct and only possible emission, so a + // count of 0 is admitted exactly while the applied counters say no unit was touched. + // + // THE ONE ADMITTED SILENCE, and it is the A/B rather than a hole: a client whose sampler + // subsystem bit is CLEAR emits neither record, the backend then runs its pre-handle + // frontend walk, and there is no window to check because there is no window. That is + // "both counts are still 0", and it is distinguishable from the narrowing this refuses + // (a narrowed window carries a non-zero count that is merely too small). The moment + // either set has been received, the rule binds. + void CheckUnitWindows(const MG_Pipe::MGPipeApplierState& st, const char* verb) { + if (st.SamplerViewCount == 0 && st.SamplerStateCount == 0) return; + const Int maxTouched = MG_Pipe::gPipeInputs.GetMaxTouchedTextureUnit(); + if (maxTouched < 0) return; + const Uint32 required = static_cast(maxTouched) + 1u; + if (st.SamplerViewStart != 0 || st.SamplerViewCount < required) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"SetSamplerViews.Count\"} - %s applies with " + "units 0..%d touched, so set_sampler_views must carry Start=0 and Count >= %u " + "(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which drops " + "the sync of at least one texture this draw samples", + verb, static_cast(maxTouched), required, st.SamplerViewStart, + st.SamplerViewCount); + std::abort(); + } + if (st.SamplerStateStart != 0 || st.SamplerStateCount < required) { + MGLOG_F("MGPipe: Fatal{ProtocolCorruption, \"BindSamplerStates.Count\"} - %s applies with " + "units 0..%d touched, so bind_sampler_states must carry Start=0 and Count >= %u " + "(CONTRACT-P5E.md §5.3); the applied window is Start=%u Count=%u, which leaves " + "an earlier draw's sampler object on at least one unit", + verb, static_cast(maxTouched), required, st.SamplerStateStart, + st.SamplerStateCount); + std::abort(); + } + } + } // namespace +#endif + Bool ServerVerbSink::OnDrawVbo(const MG_Pipe::MGPDrawInfo& info, const MG_Pipe::MGPDrawRange* ranges, const MG_Pipe::MGHostSpan* userIndices, @@ -449,6 +509,11 @@ namespace MobileGL::MG_Remote::Server { } ++m_drawRecords; +#if MOBILEGL_BUILD_DISAGGREGATED + // Ruling 19 / ID-95: the window promise, checked at every draw, before the backend is + // asked to resolve anything out of the windows. + CheckUnitWindows(MG_Pipe::MGPipeApplier(), "draw_vbo"); +#endif const MG_Backend::GlobalBackendFunctionsTable* table = Table("draw_vbo"); if (table == nullptr) return false; const MG_Backend::GLFunctionsTable& gl = table->GL; @@ -668,6 +733,10 @@ namespace MobileGL::MG_Remote::Server { // identical - and it is also the honest statement of the debt, which `rsp` counts. Bool ServerVerbSink::OnLaunchGrid(const MG_Pipe::MGPGridInfo& grid) { +#if MOBILEGL_BUILD_DISAGGREGATED + // Ruling 19 / ID-95: a dispatch samples through the same unit windows a draw does. + CheckUnitWindows(MG_Pipe::MGPipeApplier(), "launch_grid"); +#endif const MG_Backend::GlobalBackendFunctionsTable* table = Table("launch_grid"); if (table == nullptr) return false; const MG_Backend::GLFunctionsTable& gl = table->GL; diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index ff9f5351..1ff5215e 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -36,6 +36,21 @@ namespace MobileGL { // The message names the TEXTURE-side method, not this storage's: the pinned // surface list (MG_Test/Wire/RemoteClientTest.cpp's RemoteGuards) is written // in TextureObjectMipmap's vocabulary. + // + // P5e (tx2): THE READ-ONLY SHAPE ACCESSORS JOIN THE LIST. P5c guarded the + // MUTATORS and deliberately exempted the shape reads, with the reason written + // into RemoteClientTest: "the per-draw binding walk reads them every draw", so + // guarding them would have aborted a lane that was still correct. P5e is the + // commit that makes that untrue - the unit work list, the clean gate and the + // three sync bodies read the resource RECORD and the server's staged-texture + // store, and nothing on the draw path asks the frontend object for a level count, + // a level extent, a level byte size or a compressed level again. So the exemption + // becomes a guard, and reverting any handle arm aborts here BY ACCESSOR NAME + // instead of rendering from client memory. + // + // A client-thread caller is unaffected (this is the frontend's own state, and the + // guard returns unless ServerLoop::OnApplyThread()); Magma's texture sync keeps + // its named scope exemption; the pull build compiles none of it. void RefuseLegacyTextureArmFromApplyThread(const char* surface) { if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return; if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return; @@ -97,6 +112,9 @@ namespace MobileGL { #endif SizeT MipmapStorage::GetLevelCount() const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyTextureArmFromApplyThread("GetMipmapLevelCount"); +#endif return m_data.size(); } @@ -171,16 +189,25 @@ namespace MobileGL { } GLenum MipmapStorage::GetCompressedFormat(Uint level) const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyTextureArmFromApplyThread("GetCompressedFormat"); +#endif if (level >= m_compressedFormats.size()) return GL_NONE; return m_compressedFormats[level]; } SizeT MipmapStorage::GetCompressedByteSize(Uint level) const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyTextureArmFromApplyThread("GetCompressedByteSize"); +#endif if (level >= m_compressedData.size()) return 0; return m_compressedData[level].size(); } const void* MipmapStorage::MapCompressedData(Uint level) const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyTextureArmFromApplyThread("MapCompressedMipmapData"); +#endif if (level >= m_compressedData.size()) return nullptr; return m_compressedData[level].data(); } @@ -191,6 +218,9 @@ namespace MobileGL { } GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyTextureArmFromApplyThread("GetRequestedCompressedFormat"); +#endif if (level >= m_requestedCompressedFormats.size()) return GL_NONE; return m_requestedCompressedFormats[level]; } @@ -239,12 +269,18 @@ namespace MobileGL { } IntVec3 MipmapStorage::GetTexelSize(Uint level) const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyTextureArmFromApplyThread("GetMipmapTexelSize"); +#endif auto& targetTexelSizes = m_texelSizes; if (level >= targetTexelSizes.size()) return {0, 0, 0}; return targetTexelSizes[level]; } SizeT MipmapStorage::GetByteSize(Uint level) const { +#if MOBILEGL_BUILD_DISAGGREGATED + RefuseLegacyTextureArmFromApplyThread("GetMipmapByteSize"); +#endif if (level >= m_data.size()) return 0; return m_data[level].size(); } From 859590901a7f546d6bcc24d87d69b0255bbe4007 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 08:01:22 -0400 Subject: [PATCH 2/5] [MG_Backend, MG_State] (Disaggregated): P5e tx2 - the two the lane found: Magma's blit read and the copy endpoint's own sync - The widened MipmapStorage rows are backend-kind-keyed (ruling 12's shape): Magma is lockstep through P7 and its named blit still asks a frontend texture IsComplete() (VulkanRenderer's ResolveColorBlitBinding), which the unconditional widening aborted. The MUTATOR rows stay unconditional - no backend may write a frontend texture's storage from the apply thread, and none does. - CopyTexImage2D / CopyTexSubImage2D sync their DESTINATION by handle instead of merely looking its twin up. A copy destination is not sampled by the draw program, so the sampler-view window never brings it across; the pre-P5e frontend walk over every slot of every touched unit happened to sync it as a side effect. This is the "waited texture ops" term of the named narrowing (P5e-5) being made real rather than assumed. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 20 +++++++++---- .../GLState/TextureState/MipmapStorage.cpp | 29 ++++++++++++++----- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 5654752f..87c6d954 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -9775,14 +9775,20 @@ namespace MobileGL::MG_Backend::DirectGLES { if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; const auto* dstRecord = PipeTextureRecordForHandle(dstHandle); - auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.FindByHandle(dstHandle); - if (backendTextureSlot == nullptr || *backendTextureSlot == nullptr || dstRecord == nullptr) { + // P5e (tx2): SYNCED, not merely looked up. The destination of a copy is not sampled + // by the draw program, so the per-draw unit list - which is the sampler-view window + // now - never brings it across; before P5e the frontend walk over every slot of every + // touched unit happened to sync it as a side effect. The named narrowing (P5e-5) says + // the union that still covers every texture includes "the waited texture ops", and + // this is one of them: it syncs its own endpoint by handle. + auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle); + if (!backendTexture || dstRecord == nullptr) { MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin " "or no applier record on this side", dstHandle.Slot, dstHandle.Gen); return; } - dstBackendTexture = backendTextureSlot->get(); + dstBackendTexture = backendTexture.get(); mgInternalFormat = static_cast(dstRecord->Desc.InternalFormat); } else #endif @@ -9890,14 +9896,16 @@ namespace MobileGL::MG_Backend::DirectGLES { // client's unit binding slot or the client allocator (T2/T4). if (MG_Config::Transport != MG_Config::TransportMode::Monolith) { const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst; - auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.FindByHandle(dstHandle); - if (backendTextureSlot == nullptr || *backendTextureSlot == nullptr) { + // P5e (tx2): synced by handle - see CopyTexImage2D's note. A copy destination is not + // in the sampler-view window, so nothing else on this path would build its twin. + auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle); + if (!backendTexture) { MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no " "twin on this side", dstHandle.Slot, dstHandle.Gen); return; } - dstBackendTexture = backendTextureSlot->get(); + dstBackendTexture = backendTexture.get(); } else #endif { diff --git a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp index 1ff5215e..322b9016 100644 --- a/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp +++ b/MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp @@ -65,6 +65,21 @@ namespace MobileGL { surface); std::abort(); } + + // P5e (tx2), ruling 12's shape (ID-90): THE NEW READ-ONLY ROWS ARE KEYED ON THE + // BACKEND KIND, and that is not a softening - it is the same line §6 draws for + // every other P5e guard. MAGMA KEEPS THE LOCKSTEP: a DirectVulkan server's + // records are all barriered and its blit path still asks a frontend texture + // whether it IsComplete() (VulkanRenderer's ResolveColorBlitBinding), which is a + // P7 debt and legal under P5C's rules for a client parked in its own wait. What + // P5e retires is ESPRYT's per-draw shape reads, so the new rows abort on a + // DirectGLES server and are silent on a DirectVulkan one. The MUTATOR rows above + // stay unconditional: no backend may write a frontend texture's storage from the + // apply thread, and none does. + void RefuseLegacyTextureShapeReadFromApplyThread(const char* surface) { + if (MG_Config::ActiveBackendType != BackendType::DirectGLES) return; + RefuseLegacyTextureArmFromApplyThread(surface); + } #endif // Overlapping OR abutting ([lo, hi) intervals meeting edge-to-edge) in @@ -113,7 +128,7 @@ namespace MobileGL { SizeT MipmapStorage::GetLevelCount() const { #if MOBILEGL_BUILD_DISAGGREGATED - RefuseLegacyTextureArmFromApplyThread("GetMipmapLevelCount"); + RefuseLegacyTextureShapeReadFromApplyThread("GetMipmapLevelCount"); #endif return m_data.size(); } @@ -190,7 +205,7 @@ namespace MobileGL { GLenum MipmapStorage::GetCompressedFormat(Uint level) const { #if MOBILEGL_BUILD_DISAGGREGATED - RefuseLegacyTextureArmFromApplyThread("GetCompressedFormat"); + RefuseLegacyTextureShapeReadFromApplyThread("GetCompressedFormat"); #endif if (level >= m_compressedFormats.size()) return GL_NONE; return m_compressedFormats[level]; @@ -198,7 +213,7 @@ namespace MobileGL { SizeT MipmapStorage::GetCompressedByteSize(Uint level) const { #if MOBILEGL_BUILD_DISAGGREGATED - RefuseLegacyTextureArmFromApplyThread("GetCompressedByteSize"); + RefuseLegacyTextureShapeReadFromApplyThread("GetCompressedByteSize"); #endif if (level >= m_compressedData.size()) return 0; return m_compressedData[level].size(); @@ -206,7 +221,7 @@ namespace MobileGL { const void* MipmapStorage::MapCompressedData(Uint level) const { #if MOBILEGL_BUILD_DISAGGREGATED - RefuseLegacyTextureArmFromApplyThread("MapCompressedMipmapData"); + RefuseLegacyTextureShapeReadFromApplyThread("MapCompressedMipmapData"); #endif if (level >= m_compressedData.size()) return nullptr; return m_compressedData[level].data(); @@ -219,7 +234,7 @@ namespace MobileGL { GLenum MipmapStorage::GetRequestedCompressedFormat(Uint level) const { #if MOBILEGL_BUILD_DISAGGREGATED - RefuseLegacyTextureArmFromApplyThread("GetRequestedCompressedFormat"); + RefuseLegacyTextureShapeReadFromApplyThread("GetRequestedCompressedFormat"); #endif if (level >= m_requestedCompressedFormats.size()) return GL_NONE; return m_requestedCompressedFormats[level]; @@ -270,7 +285,7 @@ namespace MobileGL { IntVec3 MipmapStorage::GetTexelSize(Uint level) const { #if MOBILEGL_BUILD_DISAGGREGATED - RefuseLegacyTextureArmFromApplyThread("GetMipmapTexelSize"); + RefuseLegacyTextureShapeReadFromApplyThread("GetMipmapTexelSize"); #endif auto& targetTexelSizes = m_texelSizes; if (level >= targetTexelSizes.size()) return {0, 0, 0}; @@ -279,7 +294,7 @@ namespace MobileGL { SizeT MipmapStorage::GetByteSize(Uint level) const { #if MOBILEGL_BUILD_DISAGGREGATED - RefuseLegacyTextureArmFromApplyThread("GetMipmapByteSize"); + RefuseLegacyTextureShapeReadFromApplyThread("GetMipmapByteSize"); #endif if (level >= m_data.size()) return 0; return m_data[level].size(); From a08b3379752b00246d9f3f588f8568942f7bf0ae Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 08:06:36 -0400 Subject: [PATCH 3/5] [MG_Test] (Disaggregated): P5e tx2 - the seven shape reads become pinned guard rows, and ABA gets its own case - RemoteClientTest's MGL_TEXTURE_GUARD_TEST block gains the seven MipmapStorage shape reads P5c deliberately exempted, with the sentence that exempted them ("the per-draw binding walk reads them every draw") replaced by the reason it is no longer true. - SanityTest gains ARecycledTextureSlotReMintsTheTwinOnTheHandleArm beside UnitMemoRefusesToDriveATwinFromAnotherTexture: the borrowed-slot list needs a pointer compare because its key is derived state; the by-handle list needs none, because a recycled slot arrives as {s, g+1} and the forward-Gen reset gives the successor a fresh twin while the predecessor's handle stops resolving. G2/G14 skip twin included. --- MobileGL/MG_Test/SanityTest.cpp | 91 ++++++++++++++++++++++ MobileGL/MG_Test/Wire/RemoteClientTest.cpp | 36 ++++++++- 2 files changed, 123 insertions(+), 4 deletions(-) diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 80beaa1a..a37edf10 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -3141,6 +3141,97 @@ TEST(DirectGLESTextureSync, UnitMemoRefusesToDriveATwinFromAnotherTexture) { MG_Impl::GLImpl::BindTexture(GL_TEXTURE_2D, 0); } +#if MOBILEGL_PIPE_PUSH +// P5e (tx2), CONTRACT-P5E §4.3: THE HANDLE ARM'S SIBLING OF THE CASE ABOVE, WITH ABA AS THE +// HAZARD INSTEAD OF A SILENT SLOT SWAP. +// +// The case above pins what the BORROWED-SLOT work list needs: a pointer compare per entry, +// because its key is derived state and a slot swap that never reached the key would drive +// texture A's twin from texture B. The by-handle work list carries no such pointer and no +// PairingsIntact, and this is the argument for why it does not have to: +// +// a handle is {slot, gen}. A slot recycled to a NEW object arrives as {s, g+1}, and +// GetOrCreateByHandle RESETS the entry on a forward generation (SlotTables.h) - so the +// successor gets a fresh twin with a fresh driver texture, and the predecessor's handle +// stops resolving. There is no window in which one handle names two objects, which is +// exactly the window the pointer compare above exists to close. +// +// THE RED: drop the forward-Gen reset (make GetOrCreate return the live entry for any +// generation) and `reMinted` below is the SAME twin as `first` - the successor inherits its +// predecessor's driver texture and its predecessor's synced serials, so the first draw that +// samples it reads the dead object's pixels and the clean gate says there is nothing to do. +TEST(DirectGLESTextureSync, ARecycledTextureSlotReMintsTheTwinOnTheHandleArm) { + using namespace MobileGL; + using namespace MobileGL::MG_Backend::DirectGLES; + ScopedDirectGLESTextureBindings scoped; // fresh GLContext + registry + binding caches + if (!TextureResourceSubsystemEnabled()) { + GTEST_SKIP() << "the texture-resource subsystem bit is clear in this configuration"; + } + + auto& registry = TextureImpl::g_backendTextureObjects; + + // Two lifetime ids standing in for two frontend textures, so the client allocator hands + // back the SAME SLOT at a higher generation for the second - which is the whole hazard. + auto firstOwner = MakeShared(0u); + const MG_Pipe::MGPipeHandle first = + MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Texture, firstOwner->GetLifetimeId()); + ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(first)); + + const auto describe = [](MG_Pipe::MGPipeHandle res, Uint32 size) { + MG_Pipe::MGPResourceDesc desc{}; + desc.Resource = res; + desc.Target = static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D); + desc.StorageKind = static_cast(TextureStorageType::Mipmap); + desc.InternalFormat = static_cast(TextureInternalFormat::RGBA8); + desc.Width = size; + desc.Height = size; + desc.Depth = 1; + desc.ArrayLayers = 1; + desc.Levels = 1; + EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc)); + }; + describe(first, 8); + + auto* firstTwin = TextureImpl::ResolveTextureTwin(first); + ASSERT_NE(firstTwin, nullptr) << "the record-first resolver declined a handle whose record it " + "had just been given"; + firstTwin->NotePushedSyncHandle(first); + + // The recycle: the client frees the slot and hands it straight back out at {s, g+1}. + MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Texture, first); + auto secondOwner = MakeShared(0u); + const MG_Pipe::MGPipeHandle second = + MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Texture, secondOwner->GetLifetimeId()); + ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(second)); + ASSERT_EQ(second.Slot, first.Slot) << "the allocator did not recycle the slot, so this case " + "is not exercising ABA at all"; + ASSERT_GT(second.Gen, first.Gen); + describe(second, 16); + + auto* reMinted = TextureImpl::ResolveTextureTwin(second); + ASSERT_NE(reMinted, nullptr); + EXPECT_NE(reMinted, firstTwin) + << "the successor at a recycled texture slot inherited its predecessor's twin: it would " + "sample the dead object's driver texture, and the twin's synced serials would tell " + "the clean gate there is nothing to do"; + EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(reMinted->PushedSyncHandle())) + << "the re-minted twin kept the dead handle, so its sync prologues would resolve the " + "predecessor's record"; + + // ...and the predecessor's handle stops resolving, which is what makes a stale entry left in + // a work list harmless: its record lookup answers null and the entry is skipped. + EXPECT_EQ(PipeTextureRecordForHandle(first), nullptr) + << "the predecessor's record still answers at a recycled slot"; + + MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Texture, second); +} +#else +// G2/G14: the same ctest entry exists in the pull build and skips visibly. +TEST(DirectGLESTextureSync, ARecycledTextureSlotReMintsTheTwinOnTheHandleArm) { + GTEST_SKIP() << "the by-handle texture twin is compiled only under MOBILEGL_PIPE_PUSH"; +} +#endif + namespace { // What glTexParameteri actually reached the driver, and which backend texture was bound // when it did. The G9 probe below is a WHITE-BOX assertion (ID-19): the parameter push is diff --git a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp index fc6699ea..83d02e14 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientTest.cpp +++ b/MobileGL/MG_Test/Wire/RemoteClientTest.cpp @@ -1971,10 +1971,23 @@ TEST(RemoteGuards, CapsMirrorFallbackWithNoServerBackendIsFatalByName) { // MarkStorageDirty, MarkStorageDirtyRegion, IsStorageDirty, GetStorageDirtyRegion, // GetStorageDirtyRects // -// Deliberately NOT in the list: the shape reads (GetMipmapTexelSize / GetMipmapByteSize / -// GetMipmapLevelCount / GetUploadTargets / GetTarget / IsComplete), which the pinned -// BARRIER-PULLED object-class rows still answer through the unit-object pointer -// (FieldOwnershipTest's list, P3b/P4b/P7) - the per-draw binding walk reads them every draw. +// P5e (tx2): THE SHAPE READS JOIN THE LIST, and the sentence that stood here is why they could +// not before - "the per-draw binding walk reads them every draw". P5e is the commit that makes +// that untrue: the unit work list is st.BoundSamplerViews[], the clean gate is +// IsDrawSyncCleanByRecord, and the three sync bodies read the resource record and the server's +// staged-texture store. Nothing on the draw path asks a frontend texture for a level count, a +// level extent, a level byte size or a compressed level any more, so the exemption becomes a +// guard and reverting any handle arm aborts BY ACCESSOR NAME: +// +// GetMipmapLevelCount, GetMipmapTexelSize, GetMipmapByteSize, +// GetCompressedFormat, GetCompressedByteSize, MapCompressedMipmapData, +// GetRequestedCompressedFormat +// +// Keyed on the SERVER BACKEND being DirectGLES (ruling 12's shape): Magma is lockstep through +// P7 and its named blit still asks a frontend texture IsComplete(), which is legal for a client +// parked in its own wait. Still NOT in the list: GetUploadTargets / GetTarget / IsComplete, +// which are TextureObject's own and not MipmapStorage's - their rows retire with the object +// class in P7/P9. #define MGL_TEXTURE_GUARD_TEST(Name, Id, Call) \ TEST(RemoteGuards, Name) { \ const auto child = RunInChild([] { \ @@ -2012,6 +2025,21 @@ MGL_TEXTURE_GUARD_TEST(TextureGetStorageDirtyRegionFromTheApplyThreadIsFatalByNa MGL_TEXTURE_GUARD_TEST(TextureGetStorageDirtyRectsFromTheApplyThreadIsFatalByName, 719, (void)texture.GetStorageDirtyRects(TextureUploadTarget::Texture2D, 0, nullptr, 0)) +// ---- P5e (tx2): the seven shape reads the P4a/P5c list exempted ---------------------------- +MGL_TEXTURE_GUARD_TEST(TextureGetMipmapLevelCountFromTheApplyThreadIsFatalByName, 721, + (void)texture.GetMipmapLevelCount()) +MGL_TEXTURE_GUARD_TEST(TextureGetMipmapTexelSizeFromTheApplyThreadIsFatalByName, 722, + (void)texture.GetMipmapTexelSize(TextureUploadTarget::Texture2D, 0)) +MGL_TEXTURE_GUARD_TEST(TextureGetMipmapByteSizeFromTheApplyThreadIsFatalByName, 723, + (void)texture.GetMipmapByteSize(TextureUploadTarget::Texture2D, 0)) +MGL_TEXTURE_GUARD_TEST(TextureGetCompressedFormatFromTheApplyThreadIsFatalByName, 724, + (void)texture.GetMipmapCompressedFormat(TextureUploadTarget::Texture2D, 0)) +MGL_TEXTURE_GUARD_TEST(TextureGetCompressedByteSizeFromTheApplyThreadIsFatalByName, 725, + (void)texture.GetMipmapCompressedByteSize(TextureUploadTarget::Texture2D, 0)) +MGL_TEXTURE_GUARD_TEST(TextureMapCompressedMipmapDataFromTheApplyThreadIsFatalByName, 726, + (void)texture.MapMipmapCompressedImage(TextureUploadTarget::Texture2D, 0)) +MGL_TEXTURE_GUARD_TEST(TextureGetRequestedCompressedFormatFromTheApplyThreadIsFatalByName, 727, + (void)texture.GetMipmapRequestedCompressedFormat(TextureUploadTarget::Texture2D, 0)) #undef MGL_TEXTURE_GUARD_TEST // P5c (gt, CONTRACT-P5C §6 layer 2 / audit A1): the client-side half of the gPipeInputs From 99211a4807b43e3750b2de3adddbe601a8a48049 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 08:21:48 -0400 Subject: [PATCH 4/5] [MG_Test] (Disaggregated): P5e tx2 - the ABA case asks the twin table, not the record - MGPipeApplyResourceCreate declines a TEXTURE create on a process with no P4a consumer (NoP4aConsumer), which is every unit-lane process, so the record-driven shape of this case skipped instead of running. The ABA answer is the twin TABLE's - a forward generation resets the entry - so the case now drives g_backendTextureObjects directly and gates on EsprytSlotTablesEnabled() rather than on the family bit. - And it does NOT compare the successor's twin ADDRESS against the predecessor's: the reset released the predecessor, so the allocator hands the successor the same heap address, which is precisely why an address is never an identity here. "The slot came back empty" is the assertion. --- MobileGL/MG_Test/SanityTest.cpp | 68 ++++++++++++++++----------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index a37edf10..796274c4 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -3164,38 +3164,30 @@ TEST(DirectGLESTextureSync, ARecycledTextureSlotReMintsTheTwinOnTheHandleArm) { using namespace MobileGL; using namespace MobileGL::MG_Backend::DirectGLES; ScopedDirectGLESTextureBindings scoped; // fresh GLContext + registry + binding caches - if (!TextureResourceSubsystemEnabled()) { - GTEST_SKIP() << "the texture-resource subsystem bit is clear in this configuration"; + // The {slot, gen} table is what this case is about; the FAMILY bit is not. + if (!EsprytSlotTablesEnabled()) { + GTEST_SKIP() << "the {slot, gen} twin table arm is not selected in this configuration"; } - auto& registry = TextureImpl::g_backendTextureObjects; - - // Two lifetime ids standing in for two frontend textures, so the client allocator hands - // back the SAME SLOT at a higher generation for the second - which is the whole hazard. + // ON THE REAL TEXTURE REGISTRY, not a fake one: id's DirectGLESSlotTable cases pin the + // TABLE's rule in isolation, and this pins that the table tx2's work list indexes is the one + // that keeps it. Two lifetime ids stand in for two frontend textures, so the client + // allocator hands the SAME SLOT back at a higher generation for the second - the hazard. + // + // No applier record and no SyncTextureToBackendByHandle here: MGPipeApplyResourceCreate + // declines for a texture on a process with no P4a consumer (NoP4aConsumer), which is every + // unit-lane process, and the ABA answer is the twin TABLE's rather than the record's. auto firstOwner = MakeShared(0u); const MG_Pipe::MGPipeHandle first = MG_Pipe::MGPipeSlots().Acquire(MG_Pipe::MGPipeKind::Texture, firstOwner->GetLifetimeId()); ASSERT_FALSE(MG_Pipe::MGPipeHandleIsNull(first)); - const auto describe = [](MG_Pipe::MGPipeHandle res, Uint32 size) { - MG_Pipe::MGPResourceDesc desc{}; - desc.Resource = res; - desc.Target = static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D); - desc.StorageKind = static_cast(TextureStorageType::Mipmap); - desc.InternalFormat = static_cast(TextureInternalFormat::RGBA8); - desc.Width = size; - desc.Height = size; - desc.Depth = 1; - desc.ArrayLayers = 1; - desc.Levels = 1; - EXPECT_TRUE(MG_Pipe::MGPipeApplyResourceCreate(desc)); - }; - describe(first, 8); - - auto* firstTwin = TextureImpl::ResolveTextureTwin(first); - ASSERT_NE(firstTwin, nullptr) << "the record-first resolver declined a handle whose record it " - "had just been given"; - firstTwin->NotePushedSyncHandle(first); + auto& registry = TextureImpl::g_backendTextureObjects; + auto* firstSlot = registry.GetOrCreateByHandle(first); + ASSERT_NE(firstSlot, nullptr); + if (!*firstSlot) *firstSlot = MakeShared(); + (*firstSlot)->NotePushedSyncHandle(first); + ASSERT_EQ((*firstSlot)->PushedSyncHandle().Slot, first.Slot); // The recycle: the client frees the slot and hands it straight back out at {s, g+1}. MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Texture, first); @@ -3206,22 +3198,28 @@ TEST(DirectGLESTextureSync, ARecycledTextureSlotReMintsTheTwinOnTheHandleArm) { ASSERT_EQ(second.Slot, first.Slot) << "the allocator did not recycle the slot, so this case " "is not exercising ABA at all"; ASSERT_GT(second.Gen, first.Gen); - describe(second, 16); - auto* reMinted = TextureImpl::ResolveTextureTwin(second); - ASSERT_NE(reMinted, nullptr); - EXPECT_NE(reMinted, firstTwin) + auto* secondSlot = registry.GetOrCreateByHandle(second); + ASSERT_NE(secondSlot, nullptr); + EXPECT_EQ(*secondSlot, nullptr) << "the successor at a recycled texture slot inherited its predecessor's twin: it would " - "sample the dead object's driver texture, and the twin's synced serials would tell " - "the clean gate there is nothing to do"; - EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull(reMinted->PushedSyncHandle())) + "sample the dead object's driver texture, and the twin's synced serials and noted " + "handle would tell the clean gate there is nothing to do"; + // DELIBERATELY NOT A POINTER COMPARE against the predecessor's twin: the reset released it, + // so the allocator is free to hand the successor's twin the very same heap address - which + // it does, and which is exactly why a raw address is never an identity in this tree + // (section 4.2.1). "The slot came back empty" is the assertion; "it came back at a different + // address" is a coincidence that would make this case pass for the wrong reason. + if (!*secondSlot) *secondSlot = MakeShared(); + EXPECT_TRUE(MG_Pipe::MGPipeHandleIsNull((*secondSlot)->PushedSyncHandle())) << "the re-minted twin kept the dead handle, so its sync prologues would resolve the " "predecessor's record"; // ...and the predecessor's handle stops resolving, which is what makes a stale entry left in - // a work list harmless: its record lookup answers null and the entry is skipped. - EXPECT_EQ(PipeTextureRecordForHandle(first), nullptr) - << "the predecessor's record still answers at a recycled slot"; + // a by-handle work list harmless: the lookup answers null and the entry is skipped, where a + // borrowed slot would have been replayed against the wrong object. + EXPECT_EQ(registry.FindByHandle(first), nullptr) + << "the predecessor's handle still resolves at a recycled slot"; MG_Pipe::MGPipeSlots().Free(MG_Pipe::MGPipeKind::Texture, second); } From 1c8727102c6019e89218ef701150d25283c17bea Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 18 Sep 2026 08:26:09 -0400 Subject: [PATCH 5/5] [MG_Backend] (Disaggregated): P5e tx2 - name the two entries that actually stamp the sync handle - ResolveTextureTwin is the lookup WITHOUT the sync and does not stamp it; the comment said it did. --- MobileGL/MG_Backend/DirectGLES/Managers.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 0a50b656..bec86984 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -1945,7 +1945,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // with a handle noted syncs from the record and tolerates a null frontend object, // one without it is the monolith-glue half and reads the object as it always did. // - // Stamped by SyncTextureToBackendByHandle / ResolveTextureTwin and never cleared: + // Stamped by SyncTextureToBackendByHandle and SyncMipmapsToBackendByHandle - the two + // entries that HAVE a handle - and never cleared afterwards: // a slot recycled to {s, g+1} RESETS the twin (SlotTables.h's forward-Gen rule), so // a stale handle cannot outlive the object it names. MG_Pipe::MGPipeHandle m_pushedSyncHandle = MG_Pipe::kMGPipeNullHandle;