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(); }