[Fix] (Metrics, DirectGLES, DirectVulkan): stop the summary line printing window totals under a per-frame label, and wire the six staging paths the site inventory claimed were covered or absent

- A window with no Present divided by a faked 1 and printed the window TOTALS under
  "bytes/f[...]": the *MultiDraw* slice (47 draws, no present) reported 1,404,550 bytes as a
  PER-FRAME figure, a 47x overstatement of exactly the SEG_STAGE sizing input plan B section
  8.2 / section 11 P0 asks this package to produce. FormatWindowLine now relabels the bracket
  to "bytes[...]" and prints draws/f=n/a when the window holds no frame; acc/draw=n/a follows
  the same rule, because "0.00" beside a non-zero acc= is the same lie. Pinned by
  PipeStatsTest.SummaryLineSurvivesZeroFrames and the reworked SummaryLineSurvivesZeroDraws.
- Every per-frame and per-draw field now goes through one FormatFixed2 helper. draws/f read 1
  for 26 draws over 14 frames (1.86) and buf read 97 for 1360 bytes (97.14): a systematic
  downward truncation of up to a whole unit on the figures the package exists to produce.
  Pinned by PipeStatsTest.PerFrameFieldsKeepTwoDecimals.
- FormatSummaryLine rewrote the window bases as a side effect of formatting, so any second
  reader silently zeroed the next window. Split into a pure FormatWindowLine() and an explicit
  AdvanceSummaryWindow(); EmitSummaryLine calls both. Pinned by
  PipeStatsTest.FormattingTwiceDoesNotConsumeTheWindow.
- Init() called ResetForTesting(), against the header's own "not used by any shipping path".
  Both now forward to an internal ResetCounters().
- TracyPlot published only the MISS half of each gate under the gate's unqualified name, so
  the headline output channel of section 11 P0 carried no denominator. Two series per gate now
  ("...-hit" / "...-miss") from static literal arrays. The payload histogram stays unplotted
  and says why: it is a run-total distribution over draws (section 4.5.7), not a per-frame
  scalar, and it reaches the operator through the JSON dump.
- GetOrCreatePipeline's 15-call tally sat ABOVE the list-topology primitive-restart refusal,
  so a declined draw added ten reads it never made - an OVER-count, which breaks the lower
  bound contract every other tally keeps. Moved to immediately before the payload build, and
  the enumeration reconciled with the constant: the excluded read is the sample-shading
  capability, short-circuited by m_sampleRateShadingFeatureEnabled.
- Six real staging paths were uncounted while the inventory claimed coverage. The inventory
  claim "DirectVulkan's own buffer staging ... has no second copy to count" was simply false.
  Now wired: MultiDraw.cpp's UploadScratch/UploadScratchRing (indirect commands, the compute
  tier's draw-info array, the rebased index stream - the class is passed in, so a new tier
  cannot forget it); Managers.cpp's pool-recycle reseed and the VBO-backed Float64 narrowing;
  VkBufferManager's eight host->device copies of buffer contents plus UploadTransient, the
  single chokepoint for Magma's per-draw vertex/index/indirect staging; VkTextureManager's
  packed staging slice, with the same box/rect SHAPE split Espryt already reported.
- New byte class stage-indirect-cmd, for draw PARAMETER bytes a backend synthesises and
  stages. Kept out of stage-index-client because these are the population that becomes MGPipe
  command-record payload (section 4.5.7), not resource bytes. A name addition, not a rename:
  no recorded baseline is invalidated.
- stage-ubo-named moved past the zero-copy direct-bind decision in ResolveUniformBufferPayload
  (a direct bind repacks nothing, so counting it there reported a copy that never happened),
  and Magma's default-uniform-block image now feeds stage-ubo-global the way Espryt's does,
  counted after the per-frame slice memo.
- The site inventory in PipeStats.cpp is rewritten to name every unwired path by file and
  function. An inventory that overstates coverage is worse than a missing counter, because
  the zero is then read as an answer.
- Evidence, lavapipe/llvmpipe, MOBILEGL_PIPE_STATS=1: the MultiDraw slice now prints
  "window=0 draws/f=n/a bytes[buf=1404550 ...]"; the indirect tiers
  (MOBILEGL_ESPRYT_MULTIDRAW_MODE=indirect|multiindirect) move icmd 0 -> 660; Magma's GuiBatch
  line moves from buf=0 tex=0 ubog=0 to buf=685.71 tex=41.14 ubog=157.71 with tex[emit=9
  box=9 rect=0 jobs=9]. Off-path A/B against the base tree with the env unset, 9 runs each of
  a 40-scenario draw slice, sorted totals in ms: Espryt base 389..794 (median 399) vs branch
  384..403 (median 394); Magma base 406..472 (median 410) vs branch 408..761 (median 414) -
  the always-compiled guard is below this harness's noise on both backends.
- 1410 unit tests green (15 PipeStatsTest). integration-gpu 860/860, 860/860, 859/860; the one
  failure is DirectVulkan.PointSizeDemotion...TheDemotionIsActuallyArmedWhenTheEnvironmentPins-
  ItOn, a member of the load-dependent *IsActuallyArmed* flake family already present in the
  untouched base tree, and it passes 8/8 standalone here. stdio gate and gen_pipe check green.
This commit is contained in:
2026-09-05 20:16:49 -04:00
parent 7566a0b002
commit d380a01f32
9 changed files with 396 additions and 114 deletions
+28 -10
View File
@@ -32,9 +32,12 @@
// 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.
// 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.
// 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
@@ -42,9 +45,11 @@ namespace MobileGL::MG_Util::PipeStats {
// 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.
// 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, whichever upload shape was chosen.
// 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.
@@ -53,10 +58,16 @@ namespace MobileGL::MG_Util::PipeStats {
// 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 on the draw path.
// 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
@@ -126,7 +137,7 @@ namespace MobileGL::MG_Util::PipeStats {
inline Bool Enabled() { return g_pipeStatsEnabled; }
// Latches g_pipeStatsEnabled from MG_Config::Features.PipeStats and resets every
// Latches g_pipeStatsEnabled from MG_Config::Features.PipeStats and clears every
// counter. Called from MobileGL::Initialize() right after the config load.
void Init();
@@ -158,13 +169,20 @@ namespace MobileGL::MG_Util::PipeStats {
const char* NameOf(CallClass callClass);
const char* NameOf(Gate gate);
// The compact fixed-format one-liner MGLOG_I prints. Same text in the log and in the
// test, so the format is pinned by a test rather than by the log reader's memory.
String FormatSummaryLine();
// 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. Not used by any shipping path.
// 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();