mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[Feat] (Pipe): mint render-state CSOs on the pipeline subset and send only the dynamic chunks that moved - the steady-state cost of the whole render-state family is now two Uint16 compares
- MG_Impl/Pipe/CsoCache.h: 64 entries, LRU, hash -> probe -> MEMCMP -> handle. The memcmp is not optional: a bare 64-bit hash equality would let a collision alias two different render states onto one CSO, which is silent wrong pixels with no gate that can see it, and Mesa's cso_cache memcmps for exactly that reason. It runs only when the pipeline version moved, so never in the steady state. Eviction emits delete_render_state and frees the client slot. - kMGPipeBehaviourNoCsoContentAddressing (bit 63) turns off the PROBE and the handle reuse, not the records: every pipeline-version change then mints, binds and evicts, which is the whole-block content addressing the design is measured against. - The validate point's step 3: bind_render_state when the pipeline version moved (12 bytes, no hashing, no blob when the cache hits), set_dynamic_state when m_version moved, carrying only the dynamic chunks that differ from the tracker's staging mirror. An EMPTY chunk mask still sends the 32-byte header, because the version is what Magma's dynamic tail gates on and it moved. - The residual fill now skips a field a P2 call supplies, driven by the generated kMGPipeFieldEmittedBy[] and gated per subsystem on the runtime MOBILEGL_PIPE_PUSH bitmask, so the bitmask is a true per-subsystem A/B. THE STAMP IS UNCHANGED: a stamp says "this verb published this field", which is as true of an emitted field as of a copied one, and withholding it would abort every backend read of the fields the migration just took over. - PipeStats::RecordDrawPayloadBytes has been implemented, unit-tested and called by nothing since P0. This is its first emitter. - A field that reaches PipeInputs only through MGPipeDeriveRenderStateFields is skipped only when that derivation is really there. It is package A's and is a declared stub on the p2/contract tag this branch starts from, so rather than hard-code which branch this is, the filler probes once: a sentinel in a scratch block, the mirror cleared, the derivation run, the answer latched. It stays useful after A lands - if the derivation is ever deleted the filler degrades to PULLING those fields rather than rendering a default. - integration-verify, 818 entries, green: the comparator re-reads every field from the live context at every backend read, so "the assembled block equals the live context" is now proven rather than asserted, and the entry compare has stopped being a tautology.
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
// MobileGL - MobileGL/MG_Impl/Pipe/CsoCache.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>
|
||||
|
||||
// The render-state CSO cache (ARCHITECTURE.md 4.5.2 / 5.3, P2 brief D7).
|
||||
//
|
||||
// THE LOOKUP, and the first step is the whole point:
|
||||
// 1. m_pipelineStateVersion (widened) did not move -> reuse the last handle. ZERO hashing,
|
||||
// zero probing, and nothing is emitted unless m_version also moved. That is the steady
|
||||
// state of every frame, and it is why the tracker asks the cache at all only when the
|
||||
// dirty walk says the pipeline version moved.
|
||||
// 2. moved -> hash the 396 pipeline bytes, probe, and on a hit CONFIRM WITH A MEMCMP
|
||||
// before reusing the handle. ARCHITECTURE.md 4.1 says content addressing on an
|
||||
// xxHash; a bare 64-bit equality would let a collision alias two different render
|
||||
// states onto one CSO, which is silent wrong pixels with no gate that can see it.
|
||||
// Mesa's cso_cache memcmps for the same reason. The memcmp only ever runs on a
|
||||
// pipeline-version change, i.e. never in the steady state.
|
||||
// 3. miss -> mint a slot, emit create_render_state with every pipeline chunk, then bind.
|
||||
//
|
||||
// CAPACITY 64 (ROADMAP.md P2). 64 x (8 + 8 + 396 + 8) = about 26 KB per context. ROADMAP.md
|
||||
// open question 4 says 64 is provisional and the counters retune it at P13; this ships 64
|
||||
// and publishes the mint / bind / evict counters that retune reads.
|
||||
//
|
||||
// THE NEGATIVE CONTROL. kMGPipeBehaviourNoCsoContentAddressing (bit 63 of the runtime
|
||||
// MOBILEGL_PIPE_PUSH bitmask) turns off the PROBE and the handle reuse, not the records:
|
||||
// every pipeline-version change then mints a fresh CSO, binds it and evicts, which is
|
||||
// precisely "whole-block content addressing" and reproduces the regression
|
||||
// RenderState.h records. It is what separates "push is slower" from "the CSO design is
|
||||
// slower", and CsoContentAddressingScenario (package E) is the always-on ctest that stops
|
||||
// the switch from rotting.
|
||||
//
|
||||
// Header-only for the same ownership reason as Tracker.h: the root CMakeLists.txt that
|
||||
// would name a new .cpp is package A's and is frozen behind the p2/contract tag.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <Config.h>
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace MobileGL::MG_Pipe {
|
||||
|
||||
inline constexpr SizeT kMGPipeCsoCacheCapacity = 64;
|
||||
|
||||
class MGPipeCsoCache {
|
||||
public:
|
||||
struct Counters {
|
||||
Uint64 Mints = 0; // create_render_state emissions
|
||||
Uint64 Binds = 0; // bind_render_state emissions, mint or reuse
|
||||
Uint64 Hits = 0; // a probe that found a live entry and passed the memcmp
|
||||
Uint64 Collisions = 0; // a hash hit the memcmp REJECTED - the reason it exists
|
||||
Uint64 Evictions = 0; // LRU evictions, each one a delete_render_state
|
||||
};
|
||||
|
||||
// The handle for `params`' pipeline subset. Mints and emits create_render_state on a
|
||||
// miss; emits delete_render_state for whatever it evicts to make room. `payloadBytes`
|
||||
// accumulates what went on the wire, for PipeStats::RecordDrawPayloadBytes.
|
||||
MGPipeHandle Acquire(const RenderStateParameters& params, Uint64& payloadBytes) {
|
||||
Array<Uint8, kMGPipePipelineChunkBytes> bytes;
|
||||
MGPipeGatherPipelineBytes(params, bytes.data());
|
||||
|
||||
const Bool contentAddressed =
|
||||
(MG_Config::Features.PipePush & kMGPipeBehaviourNoCsoContentAddressing) == 0;
|
||||
if (contentAddressed) {
|
||||
const Uint64 hash = MGPipeHashPipelineBytes(bytes.data());
|
||||
for (SizeT i = 0; i < m_entries.size(); ++i) {
|
||||
if (m_entries[i].Hash != hash) continue;
|
||||
if (std::memcmp(m_entries[i].Bytes.data(), bytes.data(), bytes.size()) != 0) {
|
||||
// A 64-bit collision between two DIFFERENT render states. Reusing the
|
||||
// handle here would render one state with the other's pipeline, so the
|
||||
// entry is dropped and the caller mints - correctness first, and the
|
||||
// counter says how often it happened.
|
||||
++m_counters.Collisions;
|
||||
Evict(i);
|
||||
break;
|
||||
}
|
||||
m_entries[i].LastUsed = ++m_clock;
|
||||
++m_counters.Hits;
|
||||
return m_entries[i].Cso;
|
||||
}
|
||||
return Mint(hash, bytes, payloadBytes);
|
||||
}
|
||||
// Content addressing OFF: never probe, always mint. The records still exist, so
|
||||
// the arm differs from the default one in exactly one thing - whether a handle is
|
||||
// reused - which is what makes it a control rather than a different design.
|
||||
return Mint(0, bytes, payloadBytes);
|
||||
}
|
||||
|
||||
// Context teardown, a server reset, a unit test's fixture. Emits nothing: the applier
|
||||
// is reset alongside, and a delete for a record that is about to be dropped anyway
|
||||
// would be a wire message with no reader.
|
||||
void Reset() {
|
||||
for (auto& entry : m_entries) MGPipeSlots().Free(MGPipeKind::RenderStateCso, entry.Cso);
|
||||
m_entries.clear();
|
||||
m_clock = 0;
|
||||
}
|
||||
|
||||
void ResetCounters() { m_counters = Counters{}; }
|
||||
|
||||
SizeT Size() const { return m_entries.size(); }
|
||||
const Counters& GetCounters() const { return m_counters; }
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
Uint64 Hash = 0;
|
||||
Uint64 LastUsed = 0;
|
||||
MGPipeHandle Cso = kMGPipeNullHandle;
|
||||
Array<Uint8, kMGPipePipelineChunkBytes> Bytes{};
|
||||
};
|
||||
|
||||
MGPipeHandle Mint(Uint64 hash, const Array<Uint8, kMGPipePipelineChunkBytes>& bytes,
|
||||
Uint64& payloadBytes) {
|
||||
if (m_entries.size() >= kMGPipeCsoCacheCapacity) {
|
||||
SizeT victim = 0;
|
||||
for (SizeT i = 1; i < m_entries.size(); ++i) {
|
||||
if (m_entries[i].LastUsed < m_entries[victim].LastUsed) victim = i;
|
||||
}
|
||||
Evict(victim);
|
||||
}
|
||||
|
||||
const MGPipeHandle cso = MGPipeSlots().Allocate(MGPipeKind::RenderStateCso);
|
||||
MGPRenderStateDesc desc{};
|
||||
desc.Cso = cso;
|
||||
desc.BaseCso = kMGPipeNullHandle;
|
||||
// A brand-new CSO names every pipeline chunk; the incremental form against a
|
||||
// BaseCso is what the applier's assertion allows and P3 will use once a CSO is
|
||||
// minted from a neighbour rather than from nothing.
|
||||
desc.ChunkMask = kAllPipelineChunks;
|
||||
desc.Blob.Size = kMGPipePipelineChunkBytes;
|
||||
MGPipeApplyCreateRenderState(desc, bytes.data());
|
||||
payloadBytes += sizeof(MGPRenderStateDesc) + kMGPipePipelineChunkBytes;
|
||||
|
||||
Entry entry;
|
||||
entry.Hash = hash;
|
||||
entry.LastUsed = ++m_clock;
|
||||
entry.Cso = cso;
|
||||
entry.Bytes = bytes;
|
||||
m_entries.push_back(entry);
|
||||
|
||||
++m_counters.Mints;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::RenderStateCsoMints, 1);
|
||||
}
|
||||
return cso;
|
||||
}
|
||||
|
||||
void Evict(SizeT index) {
|
||||
MGPHandleOnly handle{};
|
||||
handle.Handle = m_entries[index].Cso;
|
||||
handle.Kind = static_cast<Uint32>(MGPipeKind::RenderStateCso);
|
||||
MGPipeApplyDeleteRenderState(handle);
|
||||
MGPipeSlots().Free(MGPipeKind::RenderStateCso, m_entries[index].Cso);
|
||||
m_entries[index] = m_entries.back();
|
||||
m_entries.pop_back();
|
||||
++m_counters.Evictions;
|
||||
}
|
||||
|
||||
static constexpr Uint32 kAllPipelineChunks =
|
||||
static_cast<Uint32>((Uint64{1} << kMGPipePipelineChunkCount) - 1);
|
||||
|
||||
Vector<Entry> m_entries;
|
||||
Uint64 m_clock = 0;
|
||||
Counters m_counters;
|
||||
};
|
||||
|
||||
// The monolith's one cache, held beside the tracker. A Vector scan rather than a hash
|
||||
// map on purpose: 64 entries of Uint64 is a handful of cache lines, it is probed only
|
||||
// when the pipeline version moved, and it keeps the eviction order in the same array as
|
||||
// the content - a map would need a second structure to answer "which is oldest".
|
||||
inline MGPipeCsoCache& MGPipeCsoCacheInstance() {
|
||||
static MGPipeCsoCache cache;
|
||||
return cache;
|
||||
}
|
||||
} // namespace MobileGL::MG_Pipe
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
@@ -15,8 +15,11 @@
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_State/GLState/BufferState/BufferState.h>
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h>
|
||||
#include <MG_Impl/Pipe/CsoCache.h>
|
||||
#include <MG_Impl/Pipe/PipeFill.h>
|
||||
#include <MG_Impl/Pipe/Tracker.h>
|
||||
#include <MG_Pipe/MGPipeRenderStateSpans.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Pipe/PipeMutation.h>
|
||||
#include <Config.h>
|
||||
|
||||
@@ -34,6 +37,12 @@ namespace MobileGL::MG_Pipe {
|
||||
// accessor of the same name (P1 brief D4: no derivation logic is re-implemented
|
||||
// here, which is what keeps the copy semantically identical by construction).
|
||||
// A forwarded field has no storage and copies nothing.
|
||||
// The two doors the P2 emission step needs into PipeInputs' storage. They exist
|
||||
// only for ApplierDerivesRenderStateFields' one-shot probe below; nothing on the hot
|
||||
// path writes through them.
|
||||
static RenderStateParameters& RenderStateOf(PipeInputs& inputs) { return inputs.m_renderState; }
|
||||
static Uint32& ClearStencilOf(PipeInputs& inputs) { return inputs.m_clearStencil; }
|
||||
|
||||
static void CopyField(PipeInputs& dst, GLContext& ctx, MGPipeInputField field) {
|
||||
using F = MGPipeInputField;
|
||||
using MG_State::GLState::BufferBindPointTargets;
|
||||
@@ -596,6 +605,150 @@ namespace MobileGL::MG_Pipe {
|
||||
MGPipeFillAccess::SetVerb(inputs, MGPipeVerb::kVerbCount);
|
||||
}
|
||||
|
||||
|
||||
// ================================================================================
|
||||
// The emission step (P2 brief D1 step 3, D5, D6, D7)
|
||||
// ================================================================================
|
||||
namespace {
|
||||
// Which runtime MOBILEGL_PIPE_PUSH subsystem owns a field, through the call that now
|
||||
// supplies it. Zero means "still pulled".
|
||||
constexpr Uint64 SubsystemForEmitter(MGPipeFieldEmitter emitter) {
|
||||
switch (emitter) {
|
||||
case MGPipeFieldEmitter::BindRenderState:
|
||||
case MGPipeFieldEmitter::CreateRenderState:
|
||||
case MGPipeFieldEmitter::SetDynamicState:
|
||||
return kMGPipeSubsystemRenderState;
|
||||
case MGPipeFieldEmitter::SetPixelPackState:
|
||||
return kMGPipeSubsystemPixelPack;
|
||||
case MGPipeFieldEmitter::SetPatchState:
|
||||
return kMGPipeSubsystemPatchState;
|
||||
case MGPipeFieldEmitter::SetVertexAttribDefaults:
|
||||
return kMGPipeSubsystemVertexAttribDefaults;
|
||||
case MGPipeFieldEmitter::kNone:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Which of those subsystems THIS BUILD actually emits for. It grows one commit at a
|
||||
// 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;
|
||||
|
||||
// The fields the applier writes DIRECTLY, out of the chunk bytes it scattered. Every
|
||||
// other emitted field reaches PipeInputs only through
|
||||
// MGPipeDeriveRenderStateFields, which is why the probe below exists.
|
||||
constexpr Bool AppliedWithoutDerivation(MGPipeInputField field) {
|
||||
switch (field) {
|
||||
case MGPipeInputField::GetRenderStateParameters:
|
||||
case MGPipeInputField::GetRenderStateParametersVersion:
|
||||
case MGPipeInputField::GetPipelineStateVersion:
|
||||
case MGPipeInputField::GetPixelStoreParameters:
|
||||
case MGPipeInputField::GetPatchVertices:
|
||||
case MGPipeInputField::GetPatchDefaultOuterLevel:
|
||||
case MGPipeInputField::GetPatchDefaultInnerLevel:
|
||||
case MGPipeInputField::GetCurrentVertexAttribute:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// DOES THIS TREE'S APPLIER ACTUALLY DERIVE?
|
||||
//
|
||||
// MGPipeDeriveRenderStateFields is package A's, and on the P2 contract tag it is a
|
||||
// declared stub whose body lands in A's follow-on commit. A field that reaches
|
||||
// PipeInputs only through that derivation must NOT be skipped by the residual fill
|
||||
// while the derivation is a stub: skipping it would leave the mirror unwritten and
|
||||
// the backend reading a default.
|
||||
//
|
||||
// Rather than hard-code which branch this is, the filler asks once: it puts a
|
||||
// sentinel in a scratch block's working RenderStateParameters, clears the mirror the
|
||||
// derivation is supposed to recompute, runs the derivation, and looks. The answer is
|
||||
// latched for the process and costs one compare, once.
|
||||
//
|
||||
// It stays useful after A lands: if the derivation is ever deleted or gated off, the
|
||||
// filler degrades to PULLING those fields instead of rendering a default, which is
|
||||
// the safe direction. The verify lane and RenderStateSpansTest are what say the
|
||||
// derivation is CORRECT; this only says it is THERE.
|
||||
Bool ApplierDerivesRenderStateFields() {
|
||||
static const Bool answer = [] {
|
||||
static PipeInputs probe;
|
||||
constexpr Uint32 kSentinel = 0x5a5a5a5au;
|
||||
MGPipeFillAccess::RenderStateOf(probe).ClearStencil = kSentinel;
|
||||
MGPipeFillAccess::ClearStencilOf(probe) = 0u;
|
||||
MGPipeDeriveRenderStateFields(probe);
|
||||
const Bool derives = MGPipeFillAccess::ClearStencilOf(probe) == kSentinel;
|
||||
if (!derives) {
|
||||
MGLOG_W_ONCE("MGPipe: MGPipeDeriveRenderStateFields does not derive on this "
|
||||
"build - the render-state mirrors stay on the pull path");
|
||||
}
|
||||
return derives;
|
||||
}();
|
||||
return answer;
|
||||
}
|
||||
|
||||
constexpr Uint32 kAllDynamicChunks =
|
||||
static_cast<Uint32>((Uint64{1} << kMGPipeDynamicChunkCount) - 1);
|
||||
|
||||
// create/bind_render_state and set_dynamic_state. Returns the bytes that went on the
|
||||
// wire, for the payload histogram.
|
||||
Uint64 EmitRenderState(GLContext& ctx, Uint32 dirty, Bool freshlyPrimed) {
|
||||
MGPipeTracker& tracker = MGPipeTrackerInstance();
|
||||
const RenderStateParameters& live = ctx.GetRenderStateParameters();
|
||||
const auto version = static_cast<Uint16>(ctx.GetRenderStateParametersVersion());
|
||||
const auto pipelineVersion = static_cast<Uint16>(ctx.GetPipelineStateVersion());
|
||||
Uint64 payloadBytes = 0;
|
||||
|
||||
if (freshlyPrimed) {
|
||||
// A fresh context is a fresh server: the cache's handles name slots this
|
||||
// client's allocator is about to hand out again, so both sides start over
|
||||
// together rather than one of them remembering the other's objects.
|
||||
MGPipeCsoCacheInstance().Reset();
|
||||
MGPipeApplierReset();
|
||||
}
|
||||
|
||||
if (dirty & MGPipeDirtyBit(MGPipeDirty::NewPipelineState)) {
|
||||
const MGPipeHandle cso = MGPipeCsoCacheInstance().Acquire(live, payloadBytes);
|
||||
MGPBindRenderState bind{};
|
||||
bind.Cso = cso;
|
||||
bind.Version = version;
|
||||
bind.PipelineVersion = pipelineVersion;
|
||||
MGPipeApplyBindRenderState(bind);
|
||||
payloadBytes += sizeof(MGPBindRenderState);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::RenderStateCsoBinds, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty & MGPipeDirtyBit(MGPipeDirty::NewRenderState)) {
|
||||
// The chunk-level suppressor: only the dynamic chunks that differ from what
|
||||
// the server has. A glViewport sends chunk D0 and nothing else; a
|
||||
// glClearColor sends D2. An EMPTY mask still sends the 32-byte header,
|
||||
// because the VERSION is what Magma's dynamic tail gates on and it moved.
|
||||
const Uint32 chunkMask =
|
||||
freshlyPrimed ? kAllDynamicChunks
|
||||
: MGPipeDynamicChunksThatMoved(live, tracker.Staged());
|
||||
Array<Uint8, kMGPipeDynamicChunkBytes> blob;
|
||||
const SizeT blobBytes = MGPipeDynamicChunkBlobBytes(chunkMask);
|
||||
MGPipeGatherDynamicChunks(live, chunkMask, blob.data());
|
||||
MGPDynamicState dyn{};
|
||||
dyn.ChunkMask = chunkMask;
|
||||
dyn.Version = version;
|
||||
dyn.Blob.Size = blobBytes;
|
||||
MGPipeApplySetDynamicState(dyn, blob.data());
|
||||
payloadBytes += sizeof(MGPDynamicState) + blobBytes;
|
||||
}
|
||||
|
||||
if (dirty & (MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewRenderState))) {
|
||||
tracker.Staged() = live;
|
||||
}
|
||||
return payloadBytes;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// ---- the validate point (P2 brief D1) ----
|
||||
void MGPipeValidateForVerb(MGPipeVerb verb) {
|
||||
PipeInputs& inputs = gPipeInputs;
|
||||
@@ -628,10 +781,27 @@ namespace MobileGL::MG_Pipe {
|
||||
// The mask is computed, latched and counted here and nothing is emitted from it
|
||||
// yet: this commit is the safety net that says the walk is semantically free
|
||||
// before any field stops being pulled. The emission steps land on top of it.
|
||||
const Uint32 dirty = MGPipeTrackerInstance().Update(*ctx, verbClass);
|
||||
(void)dirty;
|
||||
MGPipeTracker& tracker = MGPipeTrackerInstance();
|
||||
const Uint32 dirty = tracker.Update(*ctx, verbClass);
|
||||
|
||||
// ---- step 4: the residual fill ----
|
||||
// ---- step 3: emission ----
|
||||
const Uint64 pushMask = MG_Config::Features.PipePush;
|
||||
Uint64 payloadBytes = 0;
|
||||
if ((pushMask & kMGPipeSubsystemRenderState) != 0 &&
|
||||
(dirty & (MGPipeDirtyBit(MGPipeDirty::NewPipelineState) |
|
||||
MGPipeDirtyBit(MGPipeDirty::NewRenderState))) != 0) {
|
||||
payloadBytes += EmitRenderState(*ctx, dirty, tracker.FreshlyPrimed());
|
||||
}
|
||||
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
|
||||
// histogram is what answers ROADMAP.md open question 4's chunk-granularity
|
||||
// retune with data instead of a guess.
|
||||
MG_Util::PipeStats::RecordDrawPayloadBytes(payloadBytes);
|
||||
}
|
||||
|
||||
// ---- step 4: the residual fill, for what an emitted call did NOT supply ----
|
||||
const Bool applierDerives = ApplierDerivesRenderStateFields();
|
||||
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
|
||||
const auto field = static_cast<MGPipeInputField>(i);
|
||||
if (!MGPipeFieldMaskHas(mask, field)) continue;
|
||||
@@ -645,7 +815,17 @@ namespace MobileGL::MG_Pipe {
|
||||
#else
|
||||
if (kMGPipeInputFieldSticky[i]) continue;
|
||||
#endif
|
||||
MGPipeFillAccess::CopyField(inputs, *ctx, field);
|
||||
// A field a P2 call now supplies is not pulled again - that second pull is
|
||||
// exactly the cost P2 exists to remove. THE STAMP IS UNCHANGED either way: a
|
||||
// stamp says "this verb published this field", which is as true of an emitted
|
||||
// field as of a copied one, and withholding it would abort every backend read of
|
||||
// the very fields the migration just took over.
|
||||
const MGPipeFieldEmitter emitter = kMGPipeFieldEmittedBy[i];
|
||||
const Uint64 subsystem = SubsystemForEmitter(emitter);
|
||||
const Bool supplied = subsystem != 0 && (subsystem & kMGPipeWiredSubsystems) != 0 &&
|
||||
(pushMask & subsystem) != 0 &&
|
||||
(applierDerives || AppliedWithoutDerivation(field));
|
||||
if (!supplied) MGPipeFillAccess::CopyField(inputs, *ctx, field);
|
||||
#if MOBILEGL_PIPE_POISON
|
||||
// The value is copied either way; only the stamp is withheld for the omitted pair.
|
||||
if (!IsOmitted(verb, field)) filled.FilledGen[i] = filled.CurrentVerbSerial;
|
||||
|
||||
@@ -186,6 +186,7 @@ namespace MobileGL::MG_Pipe {
|
||||
Reset();
|
||||
m_context = &ctx;
|
||||
}
|
||||
const Bool wasPrimed = m_primed;
|
||||
|
||||
Uint64 now[kMGPipeDirtyCount];
|
||||
const RenderStateParameters& render = ctx.GetRenderStateParameters();
|
||||
@@ -285,6 +286,7 @@ namespace MobileGL::MG_Pipe {
|
||||
}
|
||||
|
||||
m_primed = true;
|
||||
m_freshlyPrimed = !wasPrimed;
|
||||
m_lastDirty = dirty;
|
||||
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
@@ -308,9 +310,11 @@ namespace MobileGL::MG_Pipe {
|
||||
m_framebufferBind.Reset();
|
||||
m_pack = PixelStoreParameters{};
|
||||
m_patch = PatchTrio{};
|
||||
m_staged = RenderStateParameters{};
|
||||
m_context = nullptr;
|
||||
m_lastDirty = 0;
|
||||
m_primed = false;
|
||||
m_freshlyPrimed = false;
|
||||
}
|
||||
|
||||
void ResetCounters() {
|
||||
@@ -337,6 +341,16 @@ namespace MobileGL::MG_Pipe {
|
||||
|
||||
Uint32 LastDirty() const { return m_lastDirty; }
|
||||
Bool Primed() const { return m_primed; }
|
||||
// True when the LAST Update was the first one after a Reset - a fresh context, or a
|
||||
// server reset. The emission step reads it to send a COMPLETE state rather than an
|
||||
// increment against a staging mirror that describes a context that is gone.
|
||||
Bool FreshlyPrimed() const { return m_freshlyPrimed; }
|
||||
|
||||
// "What the server has" (P2 brief D8). set_dynamic_state sends the dynamic chunks
|
||||
// that differ from this, which is the chunk-level suppressor; a chunk that
|
||||
// memcmp-matches is not sent at all.
|
||||
RenderStateParameters& Staged() { return m_staged; }
|
||||
const RenderStateParameters& Staged() const { return m_staged; }
|
||||
|
||||
private:
|
||||
static constexpr SizeT Index(MGPipeDirty bit) { return static_cast<SizeT>(bit); }
|
||||
@@ -357,9 +371,12 @@ namespace MobileGL::MG_Pipe {
|
||||
PixelStoreParameters m_pack{};
|
||||
PatchTrio m_patch{};
|
||||
|
||||
RenderStateParameters m_staged{};
|
||||
|
||||
const void* m_context = nullptr;
|
||||
Uint32 m_lastDirty = 0;
|
||||
Bool m_primed = false;
|
||||
Bool m_freshlyPrimed = false;
|
||||
|
||||
Uint64 m_fires[kMGPipeDirtyCount][kMGPipeVerbClassCount]{};
|
||||
Uint64 m_walks[kMGPipeVerbClassCount]{};
|
||||
|
||||
Reference in New Issue
Block a user