mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-13 22:58:30 +09:00
260 lines
14 KiB
C++
260 lines
14 KiB
C++
// MobileGL - MobileGL/MG_Util/Metrics/PipeStats.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>
|
|
|
|
// MGPipe boundary counters (plan B section 11 "P0 - hygiene, measurement, gates and
|
|
// skeleton", and the corollary in section 2.3.1).
|
|
//
|
|
// WHAT THIS IS FOR. The disaggregation plan has to size two things it cannot size by
|
|
// reading the tree: how many BYTES cross the frontend/backend boundary per frame (that
|
|
// sizes SEG_STAGE and the command segment), and how many accessor CALLS and memo-gate
|
|
// probes the backends actually execute per draw (that decides whether pushing state is
|
|
// cheaper than pulling it at all). Section 2.3.1 makes the second one the load-bearing
|
|
// number: the static call-site counts everyone quoted - Espryt 124 / Magma 169 - are NOT
|
|
// the dynamic per-draw cost, because every one of those paths is memo-gated, and the real
|
|
// steady state is believed to be 10-25 accessor calls per backend per draw. Without a
|
|
// dynamic counter the P2 verdict stays a guess.
|
|
//
|
|
// COST WHEN OFF. g_pipeStatsEnabled is a plain global Bool latched once at Init() from
|
|
// MG_Config::Features.PipeStats (MOBILEGL_PIPE_STATS). Every counting site in the two
|
|
// backends is written as
|
|
//
|
|
// if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::Add...(...);
|
|
//
|
|
// so with the feature off a site costs one load of a hot global plus one never-taken,
|
|
// perfectly-predicted branch, and none of the counter state is touched. The counters
|
|
// themselves are relaxed atomics rather than plain integers because texture and buffer
|
|
// staging can be reached from more than one thread; relaxed adds cost nothing extra on the
|
|
// off path, which never reaches them. The off-path cost is not a guess: see the paired
|
|
// A/B in the branch's evidence.
|
|
//
|
|
// WHAT IS COUNTED AND WHAT IS NOT: see the site inventory in PipeStats.cpp. That inventory
|
|
// is the contract - it names every path that is NOT wired, because a byte class that reads
|
|
// zero while a real copy runs uncounted is worse than a missing counter.
|
|
namespace MobileGL::MG_Util::PipeStats {
|
|
|
|
// Byte classes. Every one of these names a population of bytes that would have to be
|
|
// MOVED across the boundary once the backend no longer shares an address space with
|
|
// the frontend, which is why they are grouped this way rather than by call site.
|
|
enum class ByteClass : Uint32 {
|
|
// Buffer object contents flushed to the driver: glBufferData / glBufferSubData /
|
|
// map-write ranges / the persistent upload ring (Espryt), and every host->device
|
|
// copy of a buffer object's contents (Magma).
|
|
StageBuffer = 0,
|
|
// Texel bytes handed to glTexSubImage & friends / packed into the Vulkan upload
|
|
// staging slice, whichever upload shape was chosen.
|
|
StageTexture,
|
|
// The default-uniform-block ("global UBO") image, uploaded at most once per program
|
|
// per frame.
|
|
StageUboGlobal,
|
|
// Named uniform-block bytes that a backend has to repack itself, i.e. Magma's UBO
|
|
// ring. Espryt binds the frontend buffer straight to the driver and contributes
|
|
// nothing here - which is exactly the asymmetry D-B8 is about.
|
|
StageUboNamed,
|
|
// Client-memory vertex arrays uploaded into a scratch VBO / transient arena slice on
|
|
// the draw path.
|
|
StageVertexClient,
|
|
// Client-memory / rewritten index data staged on the draw path.
|
|
StageIndexClient,
|
|
// Draw-parameter bytes a backend synthesises and stages for the draw itself: the
|
|
// indirect-command array and the compute path's per-draw info array. These are the
|
|
// bytes that become MGPipe command-record payload once the boundary is explicit,
|
|
// which is why they are not folded into the index class.
|
|
StageIndirectCmd,
|
|
// Bytes pushed because a persistently mapped range was published to the backend.
|
|
PersistentMapPush,
|
|
// PLACEHOLDER (plan section 6.3): the residual value block does not exist yet. The
|
|
// class is minted now so the counter names never churn; it stays at 0 until P2.
|
|
ResidualValueBlock,
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// P4a's, and THE PUSH GUARD IS NEW ON THIS ENUM: CallClass has had one since P2 and
|
|
// ByteClass has never had one, so the block is opened here rather than the member
|
|
// simply appended. Without it the pull build's two counter arrays, the name table, the
|
|
// short-name table and FormatWindowLine all resize for a class that could never leave
|
|
// zero - and the pull build has to stay symbol-identical.
|
|
//
|
|
// The bytes of every CSO BLOB the client declares in a frame: P3a's vertex-elements
|
|
// blobs (which MEASUREMENTS.md recorded as a client-side array that was never
|
|
// measured, and left to P4a to give the summary line a class for), P4a's sampler
|
|
// parameter blobs, and P4a's program archives. It is the number that says what a
|
|
// transport would actually have to move for the CSO families, as opposed to what the
|
|
// records themselves cost.
|
|
CsoBlobBytes,
|
|
#endif
|
|
Count
|
|
};
|
|
|
|
// Call classes: the dynamic per-draw cost section 2.3.1 says P2 cannot be decided
|
|
// without.
|
|
enum class CallClass : Uint32 {
|
|
// Draws that reached an instrumented backend draw-preparation entry point. The
|
|
// denominator for every "per draw" number below.
|
|
Draws = 0,
|
|
// GLContext accessor calls actually EXECUTED on the instrumented paths. Counted in
|
|
// static tallies at the ~10 hot entry points, not by wrapping all 293 call sites -
|
|
// see the inventory in PipeStats.cpp for exactly what is and is not in this number.
|
|
AccessorCalls,
|
|
// Texture upload emissions: one per (upload target, level) that actually shipped
|
|
// texels. The eventual resource_subdata record count.
|
|
TextureUploadEmissions,
|
|
// Emissions that took the union-box shape (one driver upload job).
|
|
TextureUploadBoxEmissions,
|
|
// Emissions that took the refined rect-list shape (N driver upload jobs). The
|
|
// box/rect split is the thing SSIM cannot see and the +6 ms/frame Mali cliff came
|
|
// from, so it is counted separately from the byte total.
|
|
TextureUploadRectEmissions,
|
|
// Driver upload jobs issued by those emissions: 1 per box emission, N per rect-list
|
|
// emission.
|
|
TextureUploadJobs,
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// P2's two, and they are PUSH-ONLY on purpose: a render-state CSO exists only in a
|
|
// push build, and the pull build has to stay symbol-identical (G1) - growing this
|
|
// enum there would resize the counter arrays, the name table and FormatWindowLine
|
|
// for a pair of counters that could never leave zero.
|
|
//
|
|
// Render-state CSOs MINTED: a pipeline-subset hash that missed the CsoCache and had
|
|
// to be created. The Blaze3D blend toggle is the shape this exists to answer for -
|
|
// enable/draw/disable/draw forever must mint 2 and then never mint again - and it is
|
|
// half of what a P13 retune of the 64-entry capacity reads.
|
|
RenderStateCsoMints,
|
|
// Render-state CSOs BOUND: one per bind_render_state, mint or reuse. mints/binds is
|
|
// the cache's hit rate, and it is the number the CSO content-addressing negative
|
|
// control moves.
|
|
RenderStateCsoBinds,
|
|
// P3a's, and push-only for the same reason as the two above.
|
|
//
|
|
// EVERY map_persistent EMISSION, i.e. every acquisition ATTEMPT - a mint or a decline
|
|
// - because every one of them needs an answer from the resource owner. Counted that
|
|
// way on purpose: "round trips actually taken" is 0 by construction in a monolith and
|
|
// could never go red, which is not a counter, it is a decoration. Counted as attempts
|
|
// the number is identical in both modes, it is exactly "one per storage definition",
|
|
// and a regression that acquires per DRAW instead of per definition shows up on the
|
|
// first window. Counted at the client emitter, behind the usual Enabled() predicate;
|
|
// no timer anywhere.
|
|
MapPersistentRoundtrips,
|
|
// P4a's five, push-only for the same reason as the three above, and every one of them
|
|
// counts a record that ACTUALLY WENT OUT - post-suppressor - because the number an
|
|
// operator needs is the traffic, not the number of times the emitter was asked.
|
|
//
|
|
// The four set counters are how the suppressors' hit rates become readable at all: a
|
|
// suppressor that stopped suppressing is invisible in the pixels and shows up here as
|
|
// a per-frame count that tracks the draw count instead of the state changes.
|
|
FramebufferEmissions,
|
|
SamplerViewEmissions,
|
|
SamplerStateEmissions,
|
|
ShaderImageEmissions,
|
|
// The CLIENT-side twin of Espryt's TextureUploadEmissions, which counts the same
|
|
// records on the server. Two published numbers rather than one is the whole point:
|
|
// SSIM is completely blind to the box-versus-rect upload shape, and the Mali cliff it
|
|
// hides is ~+6 ms/frame, so an emission-shape divergence has to be a difference of two
|
|
// numbers rather than something only a GPU can see.
|
|
ClientTextureUploadEmissions,
|
|
// THE TEXTURE-REMINT PULL RATE (ROADMAP open question 2; P4a final review M-A). Counted
|
|
// by Espryt once per transition in which a texture that ALREADY HAD backend storage is
|
|
// re-minted image-bindable and its defined levels are replayed from the client's shadow
|
|
// (RequireImageBindableStorage) - the reach-back a split cannot make (D-M) and the one
|
|
// ImageBindableHint exists to prevent. A texture whose hint arrived before its first
|
|
// sync is allocated image-bindable up front and never counts. `trp=` on the summary
|
|
// line; the number that decides MOBILEGL_PIPE_TEXEL_RETAIN_MB's default.
|
|
TextureRemintPulls,
|
|
#endif
|
|
Count
|
|
};
|
|
|
|
// Memo gates. Each is a place where a backend decides "nothing moved, skip the work".
|
|
// Hit == the gate short-circuited; Miss == it fell through and did the work. The six
|
|
// are exactly the ones section 2.3.1 tabulates.
|
|
enum class Gate : Uint32 {
|
|
// DirectGLES.cpp SyncRenderState: the render-state-version early-out.
|
|
EsprytRenderState = 0,
|
|
// DirectGLES.cpp SyncNeccessaryTextures: the six-value sync-list key compare.
|
|
EsprytTextureSyncList,
|
|
// DirectGLES.cpp CurrentUnitBindingsEpoch: the (context, max unit, bind generation)
|
|
// shutter over the unit walk.
|
|
EsprytUnitBindingsEpoch,
|
|
// VulkanRenderer.cpp TrySetupDrawFastPath: the whole snapshot fast path.
|
|
MagmaDrawFastPath,
|
|
// VulkanRenderer.cpp GetOrCreatePipeline: the pipeline memo.
|
|
MagmaPipelineMemo,
|
|
// VulkanRenderer.cpp ApplyDynamicDrawStateTail: the version+extent tail gate.
|
|
MagmaDynamicTail,
|
|
Count
|
|
};
|
|
|
|
// Per-draw command payload size histogram (plan section 4.5.7: SEG_CMD has to be sized
|
|
// off the DISTRIBUTION, not off a per-frame total). PLACEHOLDER in P0: MGPipe emits no
|
|
// records yet, so nothing in the backends calls RecordDrawPayloadBytes. The bucketing
|
|
// and the reporting are implemented and unit-tested so that the first generator to
|
|
// emit records only has to add the one call.
|
|
inline constexpr Uint32 kPayloadHistogramBuckets = 24;
|
|
|
|
// Frames between two summary lines when MOBILEGL_PIPE_STATS=1.
|
|
inline constexpr Uint64 kDefaultSummaryFramePeriod = 120;
|
|
// The period Init() latched from MOBILEGL_PIPE_STATS_PERIOD (kDefaultSummaryFramePeriod
|
|
// when unset); never 0.
|
|
Uint64 SummaryFramePeriod();
|
|
|
|
// The latch. Read directly by Enabled() so the off path is a global load and a
|
|
// predicted branch - do not turn this into a function call.
|
|
extern Bool g_pipeStatsEnabled;
|
|
|
|
inline Bool Enabled() { return g_pipeStatsEnabled; }
|
|
|
|
// Latches g_pipeStatsEnabled from MG_Config::Features.PipeStats and clears every
|
|
// counter. Called from MobileGL::Initialize() right after the config load.
|
|
void Init();
|
|
|
|
// Final summary line plus, if MOBILEGL_PIPE_STATS_FILE names a path, the JSON dump.
|
|
// Called from MobileGL's teardown. Idempotent.
|
|
void Shutdown();
|
|
|
|
void AddBytes(ByteClass byteClass, Uint64 bytes);
|
|
void AddCalls(CallClass callClass, Uint64 count);
|
|
void CountGate(Gate gate, Bool hit);
|
|
void RecordDrawPayloadBytes(Uint64 bytes);
|
|
|
|
// Frame boundary: publishes the frame's values to Tracy (when TRACY_ENABLE), folds them
|
|
// into the run totals, clears the frame accumulators, and every kSummaryFramePeriod
|
|
// frames emits the summary line. Called from each backend's Present().
|
|
void OnPresent();
|
|
|
|
// --- introspection, for the unit test and the JSON dump -------------------------
|
|
Uint64 FrameBytes(ByteClass byteClass);
|
|
Uint64 TotalBytes(ByteClass byteClass);
|
|
Uint64 FrameCalls(CallClass callClass);
|
|
Uint64 TotalCalls(CallClass callClass);
|
|
Uint64 TotalGateHits(Gate gate);
|
|
Uint64 TotalGateMisses(Gate gate);
|
|
Uint64 TotalPayloadBucket(Uint32 bucket);
|
|
Uint64 FrameCount();
|
|
|
|
const char* NameOf(ByteClass byteClass);
|
|
const char* NameOf(CallClass callClass);
|
|
const char* NameOf(Gate gate);
|
|
|
|
// The compact fixed-format one-liner MGLOG_I prints, covering the CURRENT window (see
|
|
// AdvanceSummaryWindow). PURE: calling it twice returns the same text and changes no
|
|
// counter, so a probe, a test or a second reporting channel can format the window
|
|
// without stealing it from the log.
|
|
String FormatWindowLine();
|
|
// Closes the current window: the run totals as of now become the base the next
|
|
// FormatWindowLine() subtracts. Emitting the line and advancing the window are separate
|
|
// on purpose - the pair used to be one function whose name promised a formatter.
|
|
void AdvanceSummaryWindow();
|
|
// The teardown dump. Run totals only: a per-frame JSON stream is a different tool.
|
|
String FormatJson();
|
|
|
|
// Test hooks, used by no shipping path. Init() clears the counters through an internal
|
|
// ResetCounters() rather than by calling ResetForTesting().
|
|
void SetEnabledForTesting(Bool enabled);
|
|
void ResetForTesting();
|
|
|
|
} // namespace MobileGL::MG_Util::PipeStats
|