Files
MobileGL/MobileGL/MG_Impl/Pipe/SetHashSuppressor.h
T
swung0x48 82b1f743a7 [MG_Pipe, MG_Impl, MG_Remote, MG_Backend] (Disaggregated): P5c rv - set_context_values retires the value-class BARRIER_PULLED rows
set_context_values (opcode 79, MGPContextValues 96B, kCtxState) carries the active texture
unit, the max touched unit, the fifteen touched-binding-point counts and the five value-class
XFB rows, emitted at validate under the subsystem bit and hash-suppressed as a whole; the
emit gate and the residual-skip gate read the same answer so they can never disagree.
MGPipeApplySetContextValues writes gPipeInputs (the PackState shape). MGPAttribValue grows
24->56B to carry the frontend's converted three views, and MGPipeApplySetVertexAttribDefaults
finally writes them (the 'ignores MGPAttribValue::ValueClass' warning dies), so
GetCurrentVertexAttribute leaves the refusal set. The three texture shutters answer the
applier's own serials under a server-stamped verb (APPLIER_DERIVED), storage answer kept
verbatim elsewhere. FieldOwnership.def: nine value-class rows become derived RECORD_SUPPLIED,
the three shutters APPLIER_DERIVED, the nine object-class rows keep their phases;
FieldOwnershipTest pins that the remaining pulls are exactly the object-class list.
gen_pipe.py gains SCAN_EXEMPT_ACCESSORS with per-name reasons and retiring phases (it repairs
the --check gate the hd merge's two G6 probes had left red).

Evidence: unit 2175/2175; integration-split 111/111; both generators' --check and
--self-test green; rsp on the Xfb capture scenario 36 -> 22; red-once - gating the emission
off turns seven named scenarios red, restoring returns 111/111. CONTRACT-P5C section 5.3.
2026-09-17 09:27:36 -04:00

120 lines
6.7 KiB
C++

// 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 <Includes.h>
// 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. P3a wires the second, SetVertexBuffers. P4a wires SetSamplerViews,
// BindSamplerStates and SetShaderImages, and APPENDS an eighth slot, SetFramebufferState -
// which leaves only SetShaderBuffers and SetStreamOutputTargets unwired, both P4b's.
//
// A WIRED SLOT PUTS A REQUIREMENT ON ITS HASH, and SetVertexBuffers is where that first
// bites: the hash has to cover EVERY input the record carries, not only the set. Its
// baseInstance is DRAW state and moves without the buffer set moving, so a hash over the
// entries alone would suppress a record whose one changed field is the fetch shift and the
// server would keep the previous one. MG_Impl/Pipe/VertexInputEmit.h's
// MGPipeVertexBufferSetContentHash mixes Start, Count and BaseInstance in for exactly that
// reason, and VertexInputEmit's base-instance pair is the test that says so.
//
// 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 <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
// One slot per kVarTail set_* (ARCHITECTURE.md 5.1's call list), PLUS
// SetFramebufferState, which is not kVarTail at all: MGPFramebufferState carries a
// ContentHash for TWO jobs - the server's render-pass memo key and the client's emission
// suppressor - and the second one needs a slot here like any other, PLUS
// SetContextValues (P5c rv), also not kVarTail: one fixed-width POD whose whole-record
// hash is the "did any covered value move" answer. The enum is
// CLIENT-ONLY and is not a wire opcode, so appending before Count is safe.
enum class MGPipeSuppressorSlot : Uint32 {
SetVertexBuffers = 0, // P3a - wired, and its hash includes BaseInstance
// P4a - WIRED. The three unit sets' suppressors are not optional and were never a
// later phase's: MGPipeTypes.h makes the pattern mandatory for every kVarTail set_*,
// because GetTextureBindGeneration() bumps on a REDUNDANT rebind - MC 26.2 rebinds the
// same sampler at every texture-unit switch - so an unsuppressed set is a
// several-hundred-byte variable-length record per batch, which is the exact regression
// the design names. What P3b/P4b owns is the ~175-line BACKEND debounce these replace
// (UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged and the two
// g_*SyncList tables); P4a wires the carrier, P3b/P4b deletes the backend copy.
SetSamplerViews, // P4a - wired (backend debounce deletion: P3b/P4b)
BindSamplerStates, // P4a - wired (backend debounce deletion: P3b/P4b)
SetShaderImages, // P4a - wired (backend debounce deletion: P3b/P4b)
SetShaderBuffers, // P4b
SetStreamOutputTargets, // P4b
SetVertexAttribDefaults, // P2 - the one consumer that is wired
SetFramebufferState, // P4a - wired
// P5c rv (CONTRACT-P5C.md §5.3). NOT kVarTail either - the same shape as
// SetFramebufferState's note: MGPContextValues is one fixed-width POD, and the
// whole-record hash IS its "did any covered value move" answer (there is deliberately
// no dirty mask in the payload - a suppressed record means "nothing moved", never
// "field invalid").
SetContextValues, // P5c rv - wired, split+transport only (PipeFill.cpp gates)
Count,
};
inline constexpr SizeT kMGPipeSuppressorSlotCount = static_cast<SizeT>(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<SizeT>(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<SizeT>(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<SizeT>(slot)];
}
private:
Array<Uint64, kMGPipeSuppressorSlotCount> m_lastEmitted{};
};
// The monolith's one suppressor, beside the tracker and the CSO cache.
inline MGPipeSetHashSuppressor& MGPipeSetHashSuppressorInstance() {
// NEVER DESTROYED, for MGPipeTrackerInstance()' reason (MG_Impl/Pipe/Tracker.h): the
// rule covers every MGPipe process singleton, not only the ones on today's death
// paths.
static MGPipeSetHashSuppressor* suppressor = new MGPipeSetHashSuppressor();
return *suppressor;
}
} // namespace MobileGL::MG_Pipe
#endif // MOBILEGL_PIPE_PUSH