mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
531 lines
28 KiB
C++
531 lines
28 KiB
C++
// MobileGL - MobileGL/MG_Util/Metrics/PipeStats.cpp
|
|
// 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
|
|
|
|
#include "PipeStats.h"
|
|
|
|
#include <Config.h>
|
|
|
|
#include <fstream>
|
|
|
|
// ---------------------------------------------------------------------------------------
|
|
// SITE INVENTORY - what these counters DO and DO NOT cover.
|
|
//
|
|
// This list is the contract. A byte class that reads 0 while a real copy runs uncounted is
|
|
// worse than a missing counter, because the zero is then read as an answer, so every path
|
|
// that moves bytes and is NOT wired is named here by file and function.
|
|
//
|
|
// Byte classes
|
|
// stage-buffer ESPRYT (DirectGLES Managers.cpp): RespecifyStorageNow's
|
|
// glBufferData, FlushPendingRangesNow's three shapes (map-write,
|
|
// glBufferSubData, upload-ring stage), and the pool-recycle reseed
|
|
// in SyncBufferObject.
|
|
// MAGMA (DirectVulkan VkBufferManager.cpp): every host->device copy
|
|
// of a buffer object's contents - SwapStorageAndUploadAll, the
|
|
// StagedRangeCopy staging fill, the in-place uploads in OnRespecify /
|
|
// OnSubData / OnFlushMappedRange, the AcquirePersistentMap seed, the
|
|
// AcquireResidentSlice initial upload and the AcquireStreamedSlice
|
|
// arena fill.
|
|
// NOT covered: bytes an app writes THROUGH a persistent map. Those
|
|
// never pass through either backend (D4/D-B4) - see
|
|
// persistent-map-push.
|
|
// stage-texture ESPRYT (Managers.cpp texture upload): the bytes of whichever of
|
|
// the three upload shapes ran (rect list / union box / whole level).
|
|
// MAGMA (VkTextureManager.cpp): the packed staging slice of an
|
|
// upload batch item set.
|
|
// NOT covered: Espryt's compressed-texture path, and both backends'
|
|
// readback (device->host) paths, which are a different direction and
|
|
// want their own class when the reverse channel of section 7 exists.
|
|
// stage-ubo-global ESPRYT (DirectGLES.cpp): the default-uniform-block image, both the
|
|
// UBO-ring memcpy and the glBufferSubData fallback.
|
|
// MAGMA (UniformManager::ResolveDynamicUboDescriptor): the same
|
|
// image, counted after the per-frame slice memo, so a frame that
|
|
// re-uses the slice correctly contributes nothing.
|
|
// stage-ubo-named DirectVulkan UniformManager::ResolveUniformBufferPayload - the
|
|
// bytes Magma repacks into its own UBO ring, counted AFTER the
|
|
// zero-copy direct-bind decision (a direct bind repacks nothing).
|
|
// Espryt contributes nothing by construction (D-B8).
|
|
// stage-vertex-client ESPRYT: BackendVertexArrayObject::SyncClientSideAttributesFor-
|
|
// DrawArrays (both the Float64-narrowing and the verbatim shapes)
|
|
// and the VBO-backed Float64->Float32 narrowing scratch upload.
|
|
// MAGMA: VkBufferManager::UploadTransient(BufferKind::Vertex), which
|
|
// is the single chokepoint for the converted-vertex-stream and
|
|
// client-array staging.
|
|
// stage-index-client ESPRYT: the primitive-restart substitution buffer, and MultiDraw's
|
|
// rewritten (rebased) index stream.
|
|
// MAGMA: VkBufferManager::UploadTransient(BufferKind::Index).
|
|
// stage-indirect-cmd ESPRYT MultiDraw.cpp: the DrawElementsIndirectCommand array staged
|
|
// for the indirect tiers, and the compute tier's per-draw info
|
|
// array. Kept out of stage-index-client because these are draw
|
|
// PARAMETERS - the population that becomes MGPipe command-record
|
|
// payload, not resource bytes.
|
|
// NOT covered: Magma builds no such array (it issues one vkCmdDraw*
|
|
// per sub-draw), so this class is Espryt-only by construction.
|
|
// persistent-map-push Not wired in P0: today a persistent map is a permanent address
|
|
// space donation (D4/D-B4) that survives the whole monolith track,
|
|
// so there is no push to count until the IPC track breaks it.
|
|
// residual-value-block Placeholder, always 0 until P2 (plan section 6.3).
|
|
//
|
|
// Call classes
|
|
// draws DirectGLES PrepareForDraw and DirectVulkan SetupDraw's entry. A
|
|
// dispatch is not a draw and is not counted.
|
|
// accessor-calls STATIC TALLIES at the instrumented entry points, NOT a wrapper
|
|
// around all 293 pGLContext-> sites. Each instrumented function adds
|
|
// the number of GLContext accessor calls that its OWN body executed
|
|
// on the path taken, and each tally sits AFTER the last early return
|
|
// that would skip those reads. Covered: PrepareForDraw's own reads,
|
|
// SyncRenderState, CaptureDrawTextureSyncKeys/CurrentUnitBindings-
|
|
// Epoch, SyncNeccessaryTextures' walk, TrySetupDrawFastPath,
|
|
// GetOrCreatePipeline and ApplyDynamicDrawStateTail. NOT covered:
|
|
// the reads inside the callees those functions invoke (buffer/VAO/
|
|
// FBO/program sync, the pipeline payload builder's ~40 reads on a
|
|
// memo miss), and every non-draw entry point. The number is
|
|
// therefore a LOWER BOUND on the per-draw accessor count, and it is
|
|
// the bound over exactly the six gates section 2.3.1 tabulates.
|
|
// texture-* Per (target, level) emission, both backends.
|
|
//
|
|
// Gates: the six of section 2.3.1, each counted exactly once per probe.
|
|
//
|
|
// READING acc/draw. The accessor tally covers the instrumented functions wherever they
|
|
// run, and three of them (SyncRenderState, the texture-key capture, SyncNeccessaryTextures)
|
|
// are also reached from NON-draw call sites - Clear, readbacks, the DSA by-name entry
|
|
// points - which the `draws` counter deliberately does not count. So acc/draw is the
|
|
// per-draw steady-state number section 2.3.1 asks for only in a DRAW-DOMINATED window; in a
|
|
// window dominated by clears and readbacks it is inflated by exactly those non-draw
|
|
// probes, and the gate hit/miss pairs are the honest reading there.
|
|
// ---------------------------------------------------------------------------------------
|
|
|
|
namespace MobileGL::MG_Util::PipeStats {
|
|
|
|
Bool g_pipeStatsEnabled = false;
|
|
|
|
namespace {
|
|
constexpr Uint32 kByteClassCount = static_cast<Uint32>(ByteClass::Count);
|
|
constexpr Uint32 kCallClassCount = static_cast<Uint32>(CallClass::Count);
|
|
constexpr Uint32 kGateCount = static_cast<Uint32>(Gate::Count);
|
|
|
|
using Counter = std::atomic<Uint64>;
|
|
|
|
Counter g_frameBytes[kByteClassCount];
|
|
Counter g_totalBytes[kByteClassCount];
|
|
Counter g_frameCalls[kCallClassCount];
|
|
Counter g_totalCalls[kCallClassCount];
|
|
Counter g_frameGateHit[kGateCount];
|
|
Counter g_totalGateHit[kGateCount];
|
|
Counter g_frameGateMiss[kGateCount];
|
|
Counter g_totalGateMiss[kGateCount];
|
|
Counter g_totalPayloadBuckets[kPayloadHistogramBuckets];
|
|
Counter g_frameCount{0};
|
|
|
|
// Window bases: the run totals as of the previous summary line. Only ever touched
|
|
// from OnPresent()/Shutdown() (the present thread), so plain integers.
|
|
Uint64 g_windowBaseBytes[kByteClassCount] = {};
|
|
Uint64 g_windowBaseCalls[kCallClassCount] = {};
|
|
Uint64 g_windowBaseGateHit[kGateCount] = {};
|
|
Uint64 g_windowBaseGateMiss[kGateCount] = {};
|
|
Uint64 g_windowBaseFrames = 0;
|
|
Bool g_shutdownDone = false;
|
|
|
|
// Frames per summary line, latched by Init() from MOBILEGL_PIPE_STATS_PERIOD.
|
|
Uint64 g_summaryPeriod = kDefaultSummaryFramePeriod;
|
|
inline void Bump(Counter& counter, Uint64 amount) {
|
|
counter.fetch_add(amount, std::memory_order_relaxed);
|
|
}
|
|
|
|
inline Uint64 Read(const Counter& counter) { return counter.load(std::memory_order_relaxed); }
|
|
|
|
// Bucket 0 is "0 bytes", bucket n>0 holds [2^(n-1), 2^n). Saturates at the last
|
|
// bucket so a pathological record cannot index out of the array.
|
|
Uint32 PayloadBucketOf(Uint64 bytes) {
|
|
if (bytes == 0) {
|
|
return 0;
|
|
}
|
|
Uint32 bucket = 1;
|
|
while (bucket + 1 < kPayloadHistogramBuckets && bytes >= (Uint64{1} << bucket)) {
|
|
++bucket;
|
|
}
|
|
return bucket;
|
|
}
|
|
|
|
// Two decimals without <iomanip>. Every per-frame and per-draw field in the summary
|
|
// goes through this: the numbers are small (a per-draw accessor count in the 10-25
|
|
// band, a per-frame byte count that sizes SEG_STAGE), so truncating integer division
|
|
// loses up to a whole unit on exactly the figures the package exists to produce.
|
|
// A zero denominator is "n/a" rather than a division by a faked 1.
|
|
String FormatFixed2(Uint64 numerator, Uint64 denominator) {
|
|
if (denominator == 0) {
|
|
return "n/a";
|
|
}
|
|
const Uint64 hundredths = (numerator * 100 + denominator / 2) / denominator;
|
|
return std::to_string(hundredths / 100) + "." + (hundredths % 100 < 10 ? "0" : "") +
|
|
std::to_string(hundredths % 100);
|
|
}
|
|
|
|
const char* const kByteClassNames[kByteClassCount] = {
|
|
"stage-buffer", "stage-texture", "stage-ubo-global",
|
|
"stage-ubo-named", "stage-vertex-client", "stage-index-client",
|
|
"stage-indirect-cmd", "persistent-map-push", "residual-value-block",
|
|
#if MOBILEGL_PIPE_PUSH
|
|
"cso-blob-bytes",
|
|
#endif
|
|
};
|
|
const char* const kCallClassNames[kCallClassCount] = {
|
|
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
|
|
"tex-upload-jobs",
|
|
#if MOBILEGL_PIPE_PUSH
|
|
"render-state-cso-mints", "render-state-cso-binds", "map-persistent-roundtrips",
|
|
"framebuffer-emissions", "sampler-view-emissions", "sampler-state-emissions",
|
|
"shader-image-emissions", "client-tex-upload-emissions", "tex-remint-pulls",
|
|
#endif
|
|
};
|
|
const char* const kGateNames[kGateCount] = {
|
|
"espryt-render-state", "espryt-texture-sync-list", "espryt-unit-bindings-epoch",
|
|
"magma-draw-fastpath", "magma-pipeline-memo", "magma-dynamic-tail",
|
|
};
|
|
// Tracy needs a stable string literal per series, and a gate is TWO series: plotting
|
|
// only the misses (which is what the first cut did) hides the denominator, and a
|
|
// gate's whole point is the ratio.
|
|
const char* const kGateHitPlotNames[kGateCount] = {
|
|
"espryt-render-state-hit", "espryt-texture-sync-list-hit", "espryt-unit-bindings-epoch-hit",
|
|
"magma-draw-fastpath-hit", "magma-pipeline-memo-hit", "magma-dynamic-tail-hit",
|
|
};
|
|
const char* const kGateMissPlotNames[kGateCount] = {
|
|
"espryt-render-state-miss", "espryt-texture-sync-list-miss", "espryt-unit-bindings-epoch-miss",
|
|
"magma-draw-fastpath-miss", "magma-pipeline-memo-miss", "magma-dynamic-tail-miss",
|
|
};
|
|
// Short forms, so the per-120-frame line stays one terminal line wide.
|
|
// "csob-blob" and not "csob": the cso[] bracket below already prints csob= for the
|
|
// render-state CSO BIND count, and two different numbers under one grep is how a
|
|
// recorded baseline stops meaning anything.
|
|
const char* const kByteClassShort[kByteClassCount] = {"buf", "tex", "ubog", "ubon", "vtxc",
|
|
"idxc", "icmd", "pmap", "resid",
|
|
#if MOBILEGL_PIPE_PUSH
|
|
"csob-blob",
|
|
#endif
|
|
};
|
|
const char* const kGateShort[kGateCount] = {"ers", "etl", "eub", "mfp", "mpm", "mdt"};
|
|
|
|
void ResetCounters() {
|
|
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
|
g_frameBytes[i].store(0, std::memory_order_relaxed);
|
|
g_totalBytes[i].store(0, std::memory_order_relaxed);
|
|
g_windowBaseBytes[i] = 0;
|
|
}
|
|
for (Uint32 i = 0; i < kCallClassCount; ++i) {
|
|
g_frameCalls[i].store(0, std::memory_order_relaxed);
|
|
g_totalCalls[i].store(0, std::memory_order_relaxed);
|
|
g_windowBaseCalls[i] = 0;
|
|
}
|
|
for (Uint32 i = 0; i < kGateCount; ++i) {
|
|
g_frameGateHit[i].store(0, std::memory_order_relaxed);
|
|
g_totalGateHit[i].store(0, std::memory_order_relaxed);
|
|
g_frameGateMiss[i].store(0, std::memory_order_relaxed);
|
|
g_totalGateMiss[i].store(0, std::memory_order_relaxed);
|
|
g_windowBaseGateHit[i] = 0;
|
|
g_windowBaseGateMiss[i] = 0;
|
|
}
|
|
for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) {
|
|
g_totalPayloadBuckets[i].store(0, std::memory_order_relaxed);
|
|
}
|
|
g_frameCount.store(0, std::memory_order_relaxed);
|
|
g_windowBaseFrames = 0;
|
|
}
|
|
|
|
void EmitSummaryLine() {
|
|
const String line = FormatWindowLine();
|
|
// MGLOG_I on purpose, against the project's usual "MGLOG_D for anything
|
|
// non-critical" rule: the line has to survive an INFO build (that is the only
|
|
// build a device ever runs), it is emitted at most once per 120 frames, and it
|
|
// exists at all only when the operator set MOBILEGL_PIPE_STATS=1. It is an
|
|
// opt-in measurement channel, not per-frame noise.
|
|
MGLOG_I("%s", line.c_str());
|
|
AdvanceSummaryWindow();
|
|
}
|
|
|
|
void WriteJsonDump() {
|
|
const String& path = MG_Config::Features.PipeStatsFile;
|
|
if (path.empty()) {
|
|
return;
|
|
}
|
|
std::ofstream out(path, std::ios::out | std::ios::trunc);
|
|
if (!out) {
|
|
MGLOG_W("PipeStats: could not open MOBILEGL_PIPE_STATS_FILE='%s' for writing", path.c_str());
|
|
return;
|
|
}
|
|
out << FormatJson();
|
|
out.flush();
|
|
if (!out) {
|
|
MGLOG_W("PipeStats: failed writing MOBILEGL_PIPE_STATS_FILE='%s'", path.c_str());
|
|
return;
|
|
}
|
|
MGLOG_I("MGPipe stats: wrote JSON dump to %s", path.c_str());
|
|
}
|
|
} // namespace
|
|
|
|
void Init() {
|
|
ResetCounters();
|
|
g_shutdownDone = false;
|
|
g_pipeStatsEnabled = MG_Config::Features.PipeStats;
|
|
g_summaryPeriod = MG_Config::Features.PipeStatsPeriod == 0
|
|
? kDefaultSummaryFramePeriod
|
|
: static_cast<Uint64>(MG_Config::Features.PipeStatsPeriod);
|
|
if (g_pipeStatsEnabled) {
|
|
MGLOG_I("MGPipe stats: counters ON (MOBILEGL_PIPE_STATS), summary every %llu frames%s%s",
|
|
static_cast<unsigned long long>(g_summaryPeriod),
|
|
MG_Config::Features.PipeStatsFile.empty() ? "" : ", JSON dump to ",
|
|
MG_Config::Features.PipeStatsFile.c_str());
|
|
}
|
|
}
|
|
|
|
Uint64 SummaryFramePeriod() { return g_summaryPeriod; }
|
|
|
|
void Shutdown() {
|
|
if (!g_pipeStatsEnabled || g_shutdownDone) {
|
|
return;
|
|
}
|
|
g_shutdownDone = true;
|
|
EmitSummaryLine();
|
|
WriteJsonDump();
|
|
}
|
|
|
|
void AddBytes(ByteClass byteClass, Uint64 bytes) {
|
|
const Uint32 index = static_cast<Uint32>(byteClass);
|
|
Bump(g_frameBytes[index], bytes);
|
|
Bump(g_totalBytes[index], bytes);
|
|
}
|
|
|
|
void AddCalls(CallClass callClass, Uint64 count) {
|
|
const Uint32 index = static_cast<Uint32>(callClass);
|
|
Bump(g_frameCalls[index], count);
|
|
Bump(g_totalCalls[index], count);
|
|
}
|
|
|
|
void CountGate(Gate gate, Bool hit) {
|
|
const Uint32 index = static_cast<Uint32>(gate);
|
|
if (hit) {
|
|
Bump(g_frameGateHit[index], 1);
|
|
Bump(g_totalGateHit[index], 1);
|
|
} else {
|
|
Bump(g_frameGateMiss[index], 1);
|
|
Bump(g_totalGateMiss[index], 1);
|
|
}
|
|
}
|
|
|
|
void RecordDrawPayloadBytes(Uint64 bytes) { Bump(g_totalPayloadBuckets[PayloadBucketOf(bytes)], 1); }
|
|
|
|
void OnPresent() {
|
|
// Every frame accumulator is EXCHANGED for zero, and the exchanged value is what gets
|
|
// plotted. A read followed by a store(0) would lose any Bump that lands in between -
|
|
// buffer and texture staging reach these counters from more than one thread - from
|
|
// the plot AND from every frame; an exchange hands every add to exactly one frame.
|
|
// Without Tracy the value is taken and dropped: the clear is still the point.
|
|
//
|
|
// One plot per counter, the frame's value. Tracy keeps the series by name, and the
|
|
// names are the static literals above, which is what TracyPlot requires. A gate is
|
|
// two series - hits and misses - because the ratio is the deliverable and a miss
|
|
// count alone cannot be read.
|
|
//
|
|
// The payload histogram is deliberately NOT plotted: it is a run-total distribution
|
|
// over draws (section 4.5.7), not a per-frame scalar, and Tracy has no histogram
|
|
// series. It reaches the operator through the JSON dump.
|
|
const auto take = [](Counter& counter) { return counter.exchange(0, std::memory_order_relaxed); };
|
|
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
|
const Uint64 value = take(g_frameBytes[i]);
|
|
(void)value;
|
|
#ifdef TRACY_ENABLE
|
|
TracyPlot(kByteClassNames[i], static_cast<Int64>(value));
|
|
#endif
|
|
}
|
|
for (Uint32 i = 0; i < kCallClassCount; ++i) {
|
|
const Uint64 value = take(g_frameCalls[i]);
|
|
(void)value;
|
|
#ifdef TRACY_ENABLE
|
|
TracyPlot(kCallClassNames[i], static_cast<Int64>(value));
|
|
#endif
|
|
}
|
|
for (Uint32 i = 0; i < kGateCount; ++i) {
|
|
const Uint64 hits = take(g_frameGateHit[i]);
|
|
const Uint64 misses = take(g_frameGateMiss[i]);
|
|
(void)hits;
|
|
(void)misses;
|
|
#ifdef TRACY_ENABLE
|
|
TracyPlot(kGateHitPlotNames[i], static_cast<Int64>(hits));
|
|
TracyPlot(kGateMissPlotNames[i], static_cast<Int64>(misses));
|
|
#endif
|
|
}
|
|
const Uint64 frames = g_frameCount.fetch_add(1, std::memory_order_relaxed) + 1;
|
|
if (frames % g_summaryPeriod == 0) {
|
|
EmitSummaryLine();
|
|
}
|
|
}
|
|
|
|
Uint64 FrameBytes(ByteClass byteClass) { return Read(g_frameBytes[static_cast<Uint32>(byteClass)]); }
|
|
Uint64 TotalBytes(ByteClass byteClass) { return Read(g_totalBytes[static_cast<Uint32>(byteClass)]); }
|
|
Uint64 FrameCalls(CallClass callClass) { return Read(g_frameCalls[static_cast<Uint32>(callClass)]); }
|
|
Uint64 TotalCalls(CallClass callClass) { return Read(g_totalCalls[static_cast<Uint32>(callClass)]); }
|
|
Uint64 TotalGateHits(Gate gate) { return Read(g_totalGateHit[static_cast<Uint32>(gate)]); }
|
|
Uint64 TotalGateMisses(Gate gate) { return Read(g_totalGateMiss[static_cast<Uint32>(gate)]); }
|
|
Uint64 TotalPayloadBucket(Uint32 bucket) {
|
|
return bucket < kPayloadHistogramBuckets ? Read(g_totalPayloadBuckets[bucket]) : 0;
|
|
}
|
|
Uint64 FrameCount() { return Read(g_frameCount); }
|
|
|
|
const char* NameOf(ByteClass byteClass) { return kByteClassNames[static_cast<Uint32>(byteClass)]; }
|
|
const char* NameOf(CallClass callClass) { return kCallClassNames[static_cast<Uint32>(callClass)]; }
|
|
const char* NameOf(Gate gate) { return kGateNames[static_cast<Uint32>(gate)]; }
|
|
|
|
String FormatWindowLine() {
|
|
// Window values: everything since the previous summary. A run total over a workload
|
|
// whose shape changes (load, then steady state) hides exactly the number P2 wants.
|
|
const Uint64 frames = Read(g_frameCount);
|
|
const Uint64 windowFrames = frames - g_windowBaseFrames;
|
|
// A window with no Present in it (teardown before the first frame, or a slice whose
|
|
// whole workload runs off-screen) has NO per-frame reading. Printing the window
|
|
// totals under a "/f" label there is how a 47x overstatement of the SEG_STAGE sizing
|
|
// input got printed as a per-frame figure; the label changes instead.
|
|
const Bool perFrame = windowFrames != 0;
|
|
|
|
Uint64 bytes[kByteClassCount];
|
|
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
|
bytes[i] = Read(g_totalBytes[i]) - g_windowBaseBytes[i];
|
|
}
|
|
Uint64 calls[kCallClassCount];
|
|
for (Uint32 i = 0; i < kCallClassCount; ++i) {
|
|
calls[i] = Read(g_totalCalls[i]) - g_windowBaseCalls[i];
|
|
}
|
|
Uint64 gateHit[kGateCount];
|
|
Uint64 gateMiss[kGateCount];
|
|
for (Uint32 i = 0; i < kGateCount; ++i) {
|
|
gateHit[i] = Read(g_totalGateHit[i]) - g_windowBaseGateHit[i];
|
|
gateMiss[i] = Read(g_totalGateMiss[i]) - g_windowBaseGateMiss[i];
|
|
}
|
|
|
|
const Uint64 draws = calls[static_cast<Uint32>(CallClass::Draws)];
|
|
const Uint64 accessorCalls = calls[static_cast<Uint32>(CallClass::AccessorCalls)];
|
|
|
|
String line = "MGPipe stats:";
|
|
line += " frames=" + std::to_string(frames);
|
|
line += " window=" + std::to_string(windowFrames);
|
|
line += " draws=" + std::to_string(draws);
|
|
line += " draws/f=" + FormatFixed2(draws, windowFrames);
|
|
line += " acc=" + std::to_string(accessorCalls);
|
|
// Same rule as the per-frame fields: a window with no draw in it has no per-draw
|
|
// number, and "0.00" next to a non-zero acc= is the same lie in a smaller font.
|
|
line += " acc/draw=" + FormatFixed2(accessorCalls, draws);
|
|
// "bytes/f[...]" only when there IS a frame to divide by; otherwise the bracket is
|
|
// labelled "bytes[...]" and carries the window totals verbatim.
|
|
line += perFrame ? " bytes/f[" : " bytes[";
|
|
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
|
if (i != 0) {
|
|
line += " ";
|
|
}
|
|
line += kByteClassShort[i];
|
|
line += "=";
|
|
line += perFrame ? FormatFixed2(bytes[i], windowFrames) : std::to_string(bytes[i]);
|
|
}
|
|
line += "] tex[emit=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadEmissions)]);
|
|
line += " box=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadBoxEmissions)]);
|
|
line += " rect=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadRectEmissions)]);
|
|
line += " jobs=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureUploadJobs)]);
|
|
#if MOBILEGL_PIPE_PUSH
|
|
// Push-only, like the two counters themselves: in a pull build there is no CSO to
|
|
// mint, and a "csom=0 csob=0" that can never be anything else is noise on the one
|
|
// line an operator greps.
|
|
line += "] cso[csom=" + std::to_string(calls[static_cast<Uint32>(CallClass::RenderStateCsoMints)]);
|
|
line += " csob=" + std::to_string(calls[static_cast<Uint32>(CallClass::RenderStateCsoBinds)]);
|
|
// P3a's persistent-map acquisition attempts, on the same bracket and for the same
|
|
// reason: it is push-only, and a window with an unexpected mpr= is the one number
|
|
// that says an adoption is happening per draw rather than per storage definition.
|
|
line += " mpr=" + std::to_string(calls[static_cast<Uint32>(CallClass::MapPersistentRoundtrips)]);
|
|
// P4a's four suppressor-visible emission counts and the client-side upload twin, on a
|
|
// bracket of their own so one grep reads the whole family. Every one of them is
|
|
// post-suppressor: a set that was resolved and then not sent does not appear here, and
|
|
// that is what makes fbe/sve/sse/sie the suppressors' hit rates rather than their call
|
|
// rates. ctu is the CLIENT's count of the same texture records Espryt's tex[emit=]
|
|
// counts on the server - the two agreeing is the whole reason both are printed.
|
|
line += "] emit[fbe=" + std::to_string(calls[static_cast<Uint32>(CallClass::FramebufferEmissions)]);
|
|
line += " sve=" + std::to_string(calls[static_cast<Uint32>(CallClass::SamplerViewEmissions)]);
|
|
line += " sse=" + std::to_string(calls[static_cast<Uint32>(CallClass::SamplerStateEmissions)]);
|
|
line += " sie=" + std::to_string(calls[static_cast<Uint32>(CallClass::ShaderImageEmissions)]);
|
|
line += " ctu=" +
|
|
std::to_string(calls[static_cast<Uint32>(CallClass::ClientTextureUploadEmissions)]);
|
|
// trp is the texture-remint pull count (ROADMAP open question 2): every one is a texture
|
|
// Espryt had already allocated and then had to re-mint image-bindable, replaying its
|
|
// levels from the client's shadow, because ImageBindableHint reached it too late.
|
|
line += " trp=" + std::to_string(calls[static_cast<Uint32>(CallClass::TextureRemintPulls)]);
|
|
#endif
|
|
line += "] gates[";
|
|
for (Uint32 i = 0; i < kGateCount; ++i) {
|
|
if (i != 0) {
|
|
line += " ";
|
|
}
|
|
line += kGateShort[i];
|
|
line += "=";
|
|
line += std::to_string(gateHit[i]);
|
|
line += "/";
|
|
line += std::to_string(gateMiss[i]);
|
|
}
|
|
line += "]";
|
|
return line;
|
|
}
|
|
|
|
void AdvanceSummaryWindow() {
|
|
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
|
g_windowBaseBytes[i] = Read(g_totalBytes[i]);
|
|
}
|
|
for (Uint32 i = 0; i < kCallClassCount; ++i) {
|
|
g_windowBaseCalls[i] = Read(g_totalCalls[i]);
|
|
}
|
|
for (Uint32 i = 0; i < kGateCount; ++i) {
|
|
g_windowBaseGateHit[i] = Read(g_totalGateHit[i]);
|
|
g_windowBaseGateMiss[i] = Read(g_totalGateMiss[i]);
|
|
}
|
|
g_windowBaseFrames = Read(g_frameCount);
|
|
}
|
|
|
|
String FormatJson() {
|
|
String json = "{\n";
|
|
json += " \"frames\": " + std::to_string(Read(g_frameCount)) + ",\n";
|
|
json += " \"bytes\": {\n";
|
|
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
|
json += " \"";
|
|
json += kByteClassNames[i];
|
|
json += "\": " + std::to_string(Read(g_totalBytes[i]));
|
|
json += (i + 1 == kByteClassCount) ? "\n" : ",\n";
|
|
}
|
|
json += " },\n \"calls\": {\n";
|
|
for (Uint32 i = 0; i < kCallClassCount; ++i) {
|
|
json += " \"";
|
|
json += kCallClassNames[i];
|
|
json += "\": " + std::to_string(Read(g_totalCalls[i]));
|
|
json += (i + 1 == kCallClassCount) ? "\n" : ",\n";
|
|
}
|
|
json += " },\n \"gates\": {\n";
|
|
for (Uint32 i = 0; i < kGateCount; ++i) {
|
|
json += " \"";
|
|
json += kGateNames[i];
|
|
json += "\": {\"hit\": " + std::to_string(Read(g_totalGateHit[i])) +
|
|
", \"miss\": " + std::to_string(Read(g_totalGateMiss[i])) + "}";
|
|
json += (i + 1 == kGateCount) ? "\n" : ",\n";
|
|
}
|
|
json += " },\n \"cmd-bytes-per-draw-histogram\": [";
|
|
for (Uint32 i = 0; i < kPayloadHistogramBuckets; ++i) {
|
|
if (i != 0) {
|
|
json += ", ";
|
|
}
|
|
json += std::to_string(Read(g_totalPayloadBuckets[i]));
|
|
}
|
|
json += "]\n}\n";
|
|
return json;
|
|
}
|
|
|
|
void SetEnabledForTesting(Bool enabled) { g_pipeStatsEnabled = enabled; }
|
|
|
|
void ResetForTesting() { ResetCounters(); }
|
|
|
|
} // namespace MobileGL::MG_Util::PipeStats
|