mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-09 04:38:30 +09:00
[Feat] (Pipe): push pixel-pack, patch and vertex-attribute-default state as their own calls, the last one behind the set-hash suppressor
- set_pixel_pack_state on NEW_PIXEL_PACK, set_patch_state on NEW_PATCH_STATE, set_vertex_attrib_defaults on NEW_VERTEX_ATTRIB_DEFAULTS, each gated on its own runtime subsystem bit so the bitmask stays a per-subsystem A/B. - MG_Impl/Pipe/SetHashSuppressor.h: seven slots, one per kVarTail set_*, with SetVertexAttribDefaults the one P2 wires. 0 is reserved for "never emitted" and a computed 0 is remapped to 1, so the first emission always goes out. The other six are the carrier for the ~175 lines of backend debounce that move in P3b/P4b; landing the mechanism now means the shape is pinned by a test rather than by a plan. - The var-tail carries only the attributes that differ from the tracker's mirror, underneath the set-hash suppression of the whole resolved set - the two suppressors answer different questions and both are cheap. Two rows of Coverage.def's emitted list CANNOT yet retire their pull, and each says why in the code rather than being silently absent: - GetPixelStoreParameters is BOTH halves of the pixel store and set_pixel_pack_state deliberately carries only PACK, so the unpack half has no carrier at all. The field keeps being pulled and the verify comparator keeps proving it. - GetCurrentVertexAttribute's three views are not bit-identical - GLContext CONVERTS between them - 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. The call is still emitted, so the wire shape, the payload bytes and the suppressor are all real; the residual fill runs after emission, so the mirror ends up correct either way. Both are contract-side defects in files this package does not own; they are reported to the integrator with the exact fix rather than worked around here.
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/CsoCache.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Impl/Pipe/SetHashSuppressor.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
@@ -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<MG_State::GLState::CurrentVertexAttributeValue, kAttribs> resolved;
|
||||
for (SizeT i = 0; i < kAttribs; ++i) resolved[i] = ctx.GetCurrentVertexAttribute(static_cast<Uint>(i));
|
||||
|
||||
const Uint64 contentHash = XXH64(resolved.data(), sizeof(resolved), 0);
|
||||
if (!MGPipeSetHashSuppressorInstance().ShouldEmit(MGPipeSuppressorSlot::SetVertexAttribDefaults,
|
||||
contentHash)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Array<MGPAttribValue, kAttribs> 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<Uint32>(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<Uint32>(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<Uint32>((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
|
||||
|
||||
@@ -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 <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.
|
||||
//
|
||||
// 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).
|
||||
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<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() {
|
||||
static MGPipeSetHashSuppressor suppressor;
|
||||
return suppressor;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -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<MG_State::GLState::CurrentVertexAttributeValue,
|
||||
MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS>;
|
||||
AttribDefaults& StagedAttribDefaults() { return m_stagedAttribs; }
|
||||
const AttribDefaults& StagedAttribDefaults() const { return m_stagedAttribs; }
|
||||
|
||||
private:
|
||||
static constexpr SizeT Index(MGPipeDirty bit) { return static_cast<SizeT>(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;
|
||||
|
||||
Reference in New Issue
Block a user