diff --git a/MobileGL/MG_Impl/Pipe/PipeFill.cpp b/MobileGL/MG_Impl/Pipe/PipeFill.cpp index 40b3845b..4cd43fca 100644 --- a/MobileGL/MG_Impl/Pipe/PipeFill.cpp +++ b/MobileGL/MG_Impl/Pipe/PipeFill.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -634,7 +635,38 @@ namespace MobileGL::MG_Pipe { // time, and a field whose emitter is not wired here keeps being pulled - so adding a // row to Coverage.def can never silently drop a field on the floor before the call // that carries it exists. - constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState; + constexpr Uint64 kMGPipeWiredSubsystems = kMGPipeSubsystemRenderState | + kMGPipeSubsystemPixelPack | + kMGPipeSubsystemPatchState | + kMGPipeSubsystemVertexAttribDefaults; + + // A field an emitted call supplies COMPLETELY, so the residual fill may stop pulling + // it. Two rows of Coverage.def's emitted list do not qualify and each has its reason + // recorded here rather than a silent absence: + // + // GetPixelStoreParameters is BOTH halves of the pixel store (m_pixelStore[0] pack + // and [1] unpack) and set_pixel_pack_state deliberately carries only PACK + // (ARCHITECTURE.md 4.6 D5, MGPipeTypes.h). The unpack half has no carrier at all, + // so the field keeps being pulled and the verify comparator keeps proving it. + // + // GetCurrentVertexAttribute's three views are NOT bit-identical: GLContext + // CONVERTS between them (SetCurrentVertexAttributeFloat writes (Int32)value into + // intValue), while MGPipeApplySetVertexAttribDefaults memcpys one Data[4] into + // all three and ignores MGPAttribValue::ValueClass, which the wire type carries + // precisely so it does not have to. Until that applier reads ValueClass the + // carrier cannot reproduce the frontend value, so the field keeps being pulled. + // The call is still emitted: the wire shape, the payload bytes and the set-hash + // suppressor are all real, and the residual fill runs AFTER emission, so the + // mirror ends up with the frontend's value either way. + constexpr Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field) { + switch (field) { + case MGPipeInputField::GetPixelStoreParameters: + case MGPipeInputField::GetCurrentVertexAttribute: + return false; + default: + return true; + } + } // The fields the applier writes DIRECTLY, out of the chunk bytes it scattered. Every // other emitted field reaches PipeInputs only through @@ -689,6 +721,69 @@ namespace MobileGL::MG_Pipe { return answer; } + + // set_pixel_pack_state. PACK only, deliberately: nothing on the far side of the + // boundary reads unpack state, and the staged-repack upload path does not even issue + // glPixelStorei (ARCHITECTURE.md 4.6 D5). + Uint64 EmitPixelPackState(GLContext& ctx) { + MGPPixelPackState pack{}; + pack.Pack = ctx.GetPixelStoreParameters(false); + MGPipeApplySetPixelPackState(pack); + return sizeof(MGPPixelPackState); + } + + // set_patch_state. The trio ALSO travels in pipeline chunk P0, and that redundancy is + // a trip wire rather than waste: the applier asserts under verify that the two + // carriers agree. 28 bytes on a state that changes about once per program. + Uint64 EmitPatchState(GLContext& ctx) { + const RenderStateParameters& live = ctx.GetRenderStateParameters(); + MGPPatchState patch{}; + patch.Vertices = live.PatchVertices; + for (SizeT i = 0; i < 4; ++i) patch.Outer[i] = live.PatchDefaultOuterLevel[i]; + for (SizeT i = 0; i < 2; ++i) patch.Inner[i] = live.PatchDefaultInnerLevel[i]; + MGPipeApplySetPatchState(patch); + return sizeof(MGPPatchState); + } + + // set_vertex_attrib_defaults, behind D11's set-hash suppressor: the RESOLVED set - all + // 32 values, all three views - is hashed on the client and the call does not go out + // when the hash has not moved. That is coalescing rule 4, and this is its one wired + // consumer in P2. + Uint64 EmitVertexAttribDefaults(GLContext& ctx) { + MGPipeTracker& tracker = MGPipeTrackerInstance(); + auto& staged = tracker.StagedAttribDefaults(); + constexpr SizeT kAttribs = MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS; + static_assert(kAttribs <= 32, "MGPVertexAttribDefaults::Mask is a Uint32"); + + Array resolved; + for (SizeT i = 0; i < kAttribs; ++i) resolved[i] = ctx.GetCurrentVertexAttribute(static_cast(i)); + + const Uint64 contentHash = XXH64(resolved.data(), sizeof(resolved), 0); + if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetVertexAttribDefaults, + contentHash)) { + return 0; + } + + Array tail{}; + MGPVertexAttribDefaults header{}; + for (SizeT i = 0; i < kAttribs; ++i) { + if (std::memcmp(&resolved[i], &staged[i], sizeof(resolved[i])) == 0) continue; + MGPAttribValue& value = tail[header.Count]; + value.Location = static_cast(i); + // ClassifyVertexAttribType resolves the float/int/uint view on the CLIENT + // (MGPipeTypes.h); the frontend keeps all three populated, so the class the + // shader input consumes is what decides which one is authoritative. + value.ValueClass = 0; + std::memcpy(value.Data, resolved[i].floatValue.data(), sizeof(value.Data)); + header.Mask |= Uint32{1} << static_cast(i); + ++header.Count; + staged[i] = resolved[i]; + } + if (header.Count == 0) return 0; + MGPipeApplySetVertexAttribDefaults(header, tail.data()); + return sizeof(MGPVertexAttribDefaults) + header.Count * sizeof(MGPAttribValue); + } + constexpr Uint32 kAllDynamicChunks = static_cast((Uint64{1} << kMGPipeDynamicChunkCount) - 1); @@ -792,6 +887,22 @@ namespace MobileGL::MG_Pipe { MGPipeDirtyBit(MGPipeDirty::NewRenderState))) != 0) { payloadBytes += EmitRenderState(*ctx, dirty, tracker.FreshlyPrimed()); } + if (tracker.FreshlyPrimed()) { + // A fresh context: what the server has is no longer what any slot last emitted. + MGPipeSetHashSuppressorInstance().InvalidateAll(); + } + if ((pushMask & kMGPipeSubsystemPixelPack) != 0 && + (dirty & MGPipeDirtyBit(MGPipeDirty::NewPixelPack)) != 0) { + payloadBytes += EmitPixelPackState(*ctx); + } + if ((pushMask & kMGPipeSubsystemPatchState) != 0 && + (dirty & MGPipeDirtyBit(MGPipeDirty::NewPatchState)) != 0) { + payloadBytes += EmitPatchState(*ctx); + } + if ((pushMask & kMGPipeSubsystemVertexAttribDefaults) != 0 && + (dirty & MGPipeDirtyBit(MGPipeDirty::NewVertexAttribDefaults)) != 0) { + payloadBytes += EmitVertexAttribDefaults(*ctx); + } if (payloadBytes != 0 && MG_Util::PipeStats::Enabled()) { // PipeStats::RecordDrawPayloadBytes has been implemented and unit-tested since // P0 and called by nothing; this is its first emitter, and the 24-bucket @@ -824,6 +935,7 @@ namespace MobileGL::MG_Pipe { const Uint64 subsystem = SubsystemForEmitter(emitter); const Bool supplied = subsystem != 0 && (subsystem & kMGPipeWiredSubsystems) != 0 && (pushMask & subsystem) != 0 && + EmittedCallSuppliesTheWholeField(field) && (applierDerives || AppliedWithoutDerivation(field)); if (!supplied) MGPipeFillAccess::CopyField(inputs, *ctx, field); #if MOBILEGL_PIPE_POISON diff --git a/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h b/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h new file mode 100644 index 00000000..20081bd2 --- /dev/null +++ b/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h @@ -0,0 +1,85 @@ +// MobileGL - MobileGL/MG_Impl/Pipe/SetHashSuppressor.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include + +// Coalescing rule 4 (ARCHITECTURE.md 5.4, P2 brief D11): every kVarTail set_* hashes the +// RESOLVED set on the client and does not emit when the hash has not moved. +// +// This is the carrier for the ~175 lines of debounce that move off the backends in P3b and +// P4b - Espryt's UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged and +// Magma's equivalents all answer "is this set the same set as last time", and every one of +// them answers it against a shape the backend rediscovered. P2 lands the MECHANISM and ONE +// real consumer (SetVertexAttribDefaults) so the shape is pinned by a test rather than by a +// plan; the other six slots exist, are unit-tested, and are wired by the phase that moves +// the set they name. +// +// A hash of 0 is reserved for "never emitted", so the first emission always goes out; a +// computed 0 is remapped to 1, which costs one collision in 2^64 an extra emission and +// never a missed one. +// +// Header-only for the same ownership reason as Tracker.h and CsoCache.h: the root +// CMakeLists.txt that would name a new .cpp belongs to package A and is frozen behind the +// p2/contract tag. +#if MOBILEGL_PIPE_PUSH +#include + +namespace MobileGL::MG_Pipe { + + // One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list). + enum class MGPipeSuppressorSlot : Uint32 { + SetVertexBuffers = 0, // P3b + SetSamplerViews, // P3b + BindSamplerStates, // P3b + SetShaderImages, // P4b + SetShaderBuffers, // P4b + SetStreamOutputTargets, // P4b + SetVertexAttribDefaults, // P2 - the one consumer that is wired + Count, + }; + + inline constexpr SizeT kMGPipeSuppressorSlotCount = static_cast(MGPipeSuppressorSlot::Count); + + class MGPipeSetHashSuppressor { + public: + // True when `contentHash` differs from what this slot last emitted, and LATCHES it. + // False means the resolved set has not moved and the call must not go out. + Bool ShouldEmit(MGPipeSuppressorSlot slot, Uint64 contentHash) { + const Uint64 latched = contentHash == 0 ? 1 : contentHash; + const SizeT index = static_cast(slot); + if (m_lastEmitted[index] == latched) return false; + m_lastEmitted[index] = latched; + return true; + } + + // A context change or a server reset: what the server has is no longer what this + // slot last emitted, so the next resolved set must go out whatever it hashes to. + void Invalidate(MGPipeSuppressorSlot slot) { m_lastEmitted[static_cast(slot)] = 0; } + + void InvalidateAll() { + for (SizeT i = 0; i < kMGPipeSuppressorSlotCount; ++i) m_lastEmitted[i] = 0; + } + + // 0 == "never emitted". Exposed for the unit test, which is what pins that the + // reserved value really is reserved. + Uint64 LastEmitted(MGPipeSuppressorSlot slot) const { + return m_lastEmitted[static_cast(slot)]; + } + + private: + Array m_lastEmitted{}; + }; + + // The monolith's one suppressor, beside the tracker and the CSO cache. + inline MGPipeSetHashSuppressor& MGPipeSetHashSuppressorInstance() { + static MGPipeSetHashSuppressor suppressor; + return suppressor; + } +} // namespace MobileGL::MG_Pipe +#endif // MOBILEGL_PIPE_PUSH diff --git a/MobileGL/MG_Impl/Pipe/Tracker.h b/MobileGL/MG_Impl/Pipe/Tracker.h index f2684a8e..493fea12 100644 --- a/MobileGL/MG_Impl/Pipe/Tracker.h +++ b/MobileGL/MG_Impl/Pipe/Tracker.h @@ -311,6 +311,7 @@ namespace MobileGL::MG_Pipe { m_pack = PixelStoreParameters{}; m_patch = PatchTrio{}; m_staged = RenderStateParameters{}; + m_stagedAttribs = AttribDefaults{}; m_context = nullptr; m_lastDirty = 0; m_primed = false; @@ -352,6 +353,14 @@ namespace MobileGL::MG_Pipe { RenderStateParameters& Staged() { return m_staged; } const RenderStateParameters& Staged() const { return m_staged; } + // The same mirror for the 32 glVertexAttrib* defaults: set_vertex_attrib_defaults + // names only the attributes that differ from it, which is the var-tail's own + // suppressor underneath D11's set-hash one. + using AttribDefaults = Array; + AttribDefaults& StagedAttribDefaults() { return m_stagedAttribs; } + const AttribDefaults& StagedAttribDefaults() const { return m_stagedAttribs; } + private: static constexpr SizeT Index(MGPipeDirty bit) { return static_cast(bit); } @@ -372,6 +381,7 @@ namespace MobileGL::MG_Pipe { PatchTrio m_patch{}; RenderStateParameters m_staged{}; + AttribDefaults m_stagedAttribs{}; const void* m_context = nullptr; Uint32 m_lastDirty = 0;