mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[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:
@@ -1538,6 +1538,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
BindBufferId(TempBufferTarget, reused);
|
||||
g_GLESFuncs.glBufferSubData(TempBufferTarget, 0, (GLsizeiptr)poolSize,
|
||||
bufferObject->MappedData());
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The pool-recycle reseed is a whole-buffer upload on the hot path,
|
||||
// not a bookkeeping detail: it moves the same bytes a fresh
|
||||
// glBufferData would.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(poolSize));
|
||||
}
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(resource->pendingMutex);
|
||||
resource->pendingRanges.clear();
|
||||
@@ -2695,6 +2702,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER,
|
||||
static_cast<GLsizeiptr>(converted.size() * sizeof(Float)),
|
||||
converted.data(), GL_STREAM_DRAW);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The VBO-backed half of the 64-bit narrowing. Same population as the
|
||||
// client-array half above: a stream the backend synthesises per draw.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
||||
static_cast<Uint64>(converted.size() * sizeof(Float)));
|
||||
}
|
||||
stream.valid = true;
|
||||
stream.sourceLifetimeId = sourceLifetimeId;
|
||||
stream.sourceChangeSerial = sourceChangeSerial;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -156,7 +157,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||
// the old contents out of the same name.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||
// statsClass: which MGPipe byte population these bytes belong to. Counted here
|
||||
// rather than at the four call sites so a new tier cannot forget it.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass) {
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
@@ -169,6 +173,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
buffer.cursor = 0;
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -183,7 +190,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
|
||||
outOffset = 0;
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
@@ -207,6 +215,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||
static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
buffer.cursor += aligned;
|
||||
return true;
|
||||
@@ -417,7 +428,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||
SizeT commandBase = 0;
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -532,7 +544,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
|
||||
SizeT indexBase = 0;
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -737,10 +750,16 @@ void main() {
|
||||
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||
|
||||
if (!EnsureComputeProgram()) return;
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
|
||||
return;
|
||||
}
|
||||
// data == nullptr: pure respecify, the compute pass writes the contents, so no
|
||||
// host bytes cross here and nothing is counted.
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
|
||||
return;
|
||||
}
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||
|
||||
@@ -2059,14 +2059,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
out.payload = outData;
|
||||
out.payloadSize = outSize;
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// D-B8: these are the bytes Magma repacks into its own UBO ring, i.e. exactly
|
||||
// the host payload a split build would have to ship with set_shader_buffers.
|
||||
// Espryt binds the frontend buffer to the driver and contributes nothing here,
|
||||
// which is why the class is named for the payload and not for the call.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed,
|
||||
static_cast<Uint64>(outSize));
|
||||
}
|
||||
|
||||
// Zero-copy direct bind: for a persistent-mapped coherent app buffer whose full reflected
|
||||
// block fits within the aligned bound range, point the descriptor straight at the app's
|
||||
@@ -2085,6 +2077,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
out.dynamicOffset = rangeStart;
|
||||
}
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled() && !out.directBindable) {
|
||||
// D-B8: the bytes Magma repacks into its own UBO ring, i.e. exactly the host
|
||||
// payload a split build would have to ship with set_shader_buffers. Espryt binds
|
||||
// the frontend buffer to the driver and contributes nothing here, which is why
|
||||
// the class is named for the payload and not for the call. Counted AFTER the
|
||||
// zero-copy direct-bind decision: a direct bind repacks nothing, and counting it
|
||||
// here reported a copy that never happened.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboNamed,
|
||||
static_cast<Uint64>(outSize));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2292,6 +2294,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outBuffer = slice.buffer;
|
||||
outRange = ubo.payloadSize;
|
||||
outDynamicOffset = static_cast<Uint32>(slice.offset);
|
||||
if (isGlobalUbo && MG_Util::PipeStats::Enabled()) {
|
||||
// Magma's half of stage-ubo-global, so the class means the same on both
|
||||
// backends. The memo hit above returns before this, so a frame that reuses the
|
||||
// slice correctly contributes nothing.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageUboGlobal,
|
||||
static_cast<Uint64>(ubo.payloadSize));
|
||||
}
|
||||
if (isGlobalUbo) {
|
||||
m_globalUboMemo[m_globalUboMemoNext] =
|
||||
GlobalUboSliceMemo{uboProgramLifetimeId, uboFrameSerial, uboContentVersion,
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "../DirectVulkan.h"
|
||||
#include "VulkanRenderer.h"
|
||||
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
constexpr VmaAllocationCreateFlags kResidentBufferAllocationFlags =
|
||||
@@ -229,8 +231,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool VkBufferManager::UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data,
|
||||
VkDeviceSize size, VkDeviceSize alignment, BufferSlice& outSlice) {
|
||||
(void)kind;
|
||||
return m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice);
|
||||
if (!m_transientUploadArena.Upload(frameIndex, data, size, alignment, outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The single chokepoint for Magma's per-draw staging. Uniform is deliberately
|
||||
// absent: its bytes are counted by the caller, which is the only place that
|
||||
// knows whether the payload is the default block (stage-ubo-global) or a named
|
||||
// one repacked into the ring (stage-ubo-named), and counting here as well would
|
||||
// double every uniform byte.
|
||||
switch (kind) {
|
||||
case BufferKind::Vertex:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Index:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndexClient,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Indirect:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageIndirectCmd,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::TextureBuffer:
|
||||
case BufferKind::ShaderStorage:
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
break;
|
||||
case BufferKind::Uniform:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkBufferManager::InitializeTransientArenas() {
|
||||
@@ -339,6 +371,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.pendingFullUpload = true;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource.pendingFullUpload = false;
|
||||
return true;
|
||||
}
|
||||
@@ -353,6 +388,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), 16, staging)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The staging fill is the host copy; the vkCmdCopyBuffer below is the device
|
||||
// half of the same bytes and is not counted twice.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
VkCommandBuffer commandBuffer = m_copyProvider->AcquireBufferCopyCommandBuffer();
|
||||
if (commandBuffer == VK_NULL_HANDLE) {
|
||||
return false;
|
||||
@@ -422,6 +462,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!resource->buffer.Upload(bufferObject.MappedData(), size, 0)) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnRespecify: in-place upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,6 +489,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnSubData: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -484,6 +529,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<VkDeviceSize>(size), static_cast<VkDeviceSize>(offset))) {
|
||||
MGLOG_E_ONCE("VkBufferManager::OnFlushMappedRange: host upload failed");
|
||||
resource->pendingFullUpload = true;
|
||||
} else if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -554,6 +602,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint8* seed = bufferObject.MappedData();
|
||||
if (seed != nullptr) {
|
||||
resource->buffer.Upload(seed, size, 0);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// The one-time seed of a persistent map. Everything the app writes AFTER
|
||||
// this goes straight through the mapping and is persistent-map-push
|
||||
// territory (unwired, D4/D-B4), not this class.
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
}
|
||||
resource->persistentMapped = true;
|
||||
resource->pendingFullUpload = false;
|
||||
@@ -602,6 +657,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource->usageFlags = 0;
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
|
||||
static_cast<Uint64>(size));
|
||||
}
|
||||
resource->pendingFullUpload = false;
|
||||
}
|
||||
|
||||
@@ -681,6 +740,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
outSlice)) {
|
||||
return false;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer, static_cast<Uint64>(size));
|
||||
}
|
||||
resource->transientSlice = outSlice;
|
||||
resource->transientFrameSerial = m_frameSerial;
|
||||
resource->transientChangeSerial = changeSerial;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/PipeStats.h"
|
||||
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
@@ -3150,6 +3151,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
packBox(dst, item.regionLo, item.regionSize);
|
||||
}
|
||||
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// Same shape split as Espryt's: one union box per item, or one job per rect of
|
||||
// a refined rect list. The box/rect decision is invisible to SSIM and is what
|
||||
// the +6 ms/frame Mali cliff of section 7.3 was, so it is counted apart from
|
||||
// the bytes.
|
||||
Uint64 boxEmissions = 0;
|
||||
Uint64 rectEmissions = 0;
|
||||
Uint64 jobs = 0;
|
||||
for (const auto& item : uploadItems) {
|
||||
if (item.rects.empty()) {
|
||||
++boxEmissions;
|
||||
jobs += isCombinedDepthStencil ? 2u : 1u;
|
||||
} else {
|
||||
++rectEmissions;
|
||||
jobs += static_cast<Uint64>(item.rects.size());
|
||||
}
|
||||
}
|
||||
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
|
||||
static_cast<Uint64>(stagingSize));
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadEmissions,
|
||||
static_cast<Uint64>(uploadItems.size()));
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions, boxEmissions);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadRectEmissions, rectEmissions);
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs, jobs);
|
||||
}
|
||||
|
||||
const VkImageAspectFlags aspectMask = GetAspectMaskForFormat(outResource.format);
|
||||
VkPipelineStageFlags uploadSrcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
|
||||
VkAccessFlags uploadSrcAccessMask = 0;
|
||||
|
||||
@@ -5168,17 +5168,6 @@ void main() {
|
||||
syntheticVertexInputState.pNext = vis.state.pNext;
|
||||
pipelineVertexInputState = &syntheticVertexInputState;
|
||||
}
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// THE payload-builder walk section 2.3.1 says only runs on a pipeline memo
|
||||
// miss. Counted as a constant: the unconditional accessor reads between here
|
||||
// and the end of the payload build (the six capability reads, the draw-FBO
|
||||
// slot, the two stencil faces, the polygon mode, sample shading + min sample
|
||||
// shading, patch vertices, the depth mask and the depth func, and the second
|
||||
// draw-FBO slot read). Reads that are themselves conditional - the cull-mode
|
||||
// ternary, the logic-op fetch, the two tessellation default-level reads - are
|
||||
// deliberately excluded, so this stays a LOWER bound like every other tally.
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 15);
|
||||
}
|
||||
auto cullFaceEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::CullFace);
|
||||
auto depthTestEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::DepthTest);
|
||||
auto polygonOffsetFillEnabled =
|
||||
@@ -5264,6 +5253,24 @@ void main() {
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
// THE payload-builder walk section 2.3.1 says only runs on a pipeline memo miss.
|
||||
// Counted as a constant, and counted HERE rather than at the top of the walk:
|
||||
// the list-topology primitive-restart refusal above returns VK_NULL_HANDLE after
|
||||
// only ten of these reads have run, and a tally that fires before an early return
|
||||
// is an OVER-count, which breaks the lower-bound contract every other tally keeps.
|
||||
//
|
||||
// The 15 are: the six capability reads (cull face, depth test, polygon offset
|
||||
// fill, rasterizer discard, colour logic op, stencil test), the draw-FBO slot
|
||||
// read that gates depth/stencil, the two stencil face states, the polygon mode,
|
||||
// the min sample shading value, the patch vertex count, the depth mask, the depth
|
||||
// func, and the second draw-FBO slot read below. The sample-shading CAPABILITY
|
||||
// read is the one excluded: it sits behind && on m_sampleRateShadingFeatureEnabled
|
||||
// and does not run on a device without the feature. The other conditional reads -
|
||||
// the cull-mode ternary, the logic-op fetch, the two tessellation default-level
|
||||
// reads - are excluded for the same reason, so this stays a LOWER bound.
|
||||
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::AccessorCalls, 15);
|
||||
}
|
||||
PipelineFactory::PipelineCreatePayload payload {
|
||||
.programHash = programObj.hash,
|
||||
.vertexInputHash = vertexLayoutHash,
|
||||
|
||||
@@ -56,6 +56,7 @@ namespace {
|
||||
// Every other class untouched, the residual-value-block placeholder included.
|
||||
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboGlobal), 0u);
|
||||
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageUboNamed), 0u);
|
||||
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::StageIndirectCmd), 0u);
|
||||
EXPECT_EQ(PS::TotalBytes(PS::ByteClass::ResidualValueBlock), 0u);
|
||||
}
|
||||
|
||||
@@ -123,12 +124,12 @@ namespace {
|
||||
PS::AddBytes(PS::ByteClass::StageBuffer, 4096);
|
||||
PS::OnPresent();
|
||||
|
||||
const String line = PS::FormatSummaryLine();
|
||||
const String line = PS::FormatWindowLine();
|
||||
EXPECT_NE(line.find("MGPipe stats:"), String::npos) << line;
|
||||
EXPECT_NE(line.find("draws=4"), String::npos) << line;
|
||||
// 50 accessor calls over 4 draws, two decimals, no <iomanip>.
|
||||
EXPECT_NE(line.find("acc/draw=12.50"), String::npos) << line;
|
||||
EXPECT_NE(line.find("buf=4096"), String::npos) << line;
|
||||
EXPECT_NE(line.find("buf=4096.00"), String::npos) << line;
|
||||
for (Uint32 i = 0; i < static_cast<Uint32>(PS::Gate::Count); ++i) {
|
||||
EXPECT_NE(line.find("="), String::npos);
|
||||
}
|
||||
@@ -136,26 +137,82 @@ namespace {
|
||||
EXPECT_NE(line.find("tex[emit="), String::npos) << line;
|
||||
}
|
||||
|
||||
// Per-frame fields carry two decimals for the same reason acc/draw does: they are small
|
||||
// and load-bearing (bytes/f sizes SEG_STAGE), and integer division silently rounds a
|
||||
// whole unit off each of them. 26 draws over 14 frames is 1.86, not 1.
|
||||
TEST_F(PipeStatsTest, PerFrameFieldsKeepTwoDecimals) {
|
||||
PS::AddCalls(PS::CallClass::Draws, 26);
|
||||
PS::AddBytes(PS::ByteClass::StageBuffer, 1360);
|
||||
for (Uint32 i = 0; i < 14; ++i) {
|
||||
PS::OnPresent();
|
||||
}
|
||||
|
||||
const String line = PS::FormatWindowLine();
|
||||
EXPECT_NE(line.find("draws/f=1.86"), String::npos) << line;
|
||||
EXPECT_NE(line.find("buf=97.14"), String::npos) << line;
|
||||
}
|
||||
|
||||
// Successive summaries report WINDOWS, not run totals: a run total over a workload that
|
||||
// changes shape (load, then steady state) averages away the very number section 2.3.1
|
||||
// wants.
|
||||
// wants. Advancing the window is an explicit call, not a side effect of formatting.
|
||||
TEST_F(PipeStatsTest, SummaryLinesReportDisjointWindows) {
|
||||
PS::AddCalls(PS::CallClass::Draws, 10);
|
||||
PS::OnPresent();
|
||||
const String first = PS::FormatSummaryLine();
|
||||
const String first = PS::FormatWindowLine();
|
||||
EXPECT_NE(first.find("draws=10"), String::npos) << first;
|
||||
PS::AdvanceSummaryWindow();
|
||||
|
||||
PS::AddCalls(PS::CallClass::Draws, 3);
|
||||
PS::OnPresent();
|
||||
const String second = PS::FormatSummaryLine();
|
||||
const String second = PS::FormatWindowLine();
|
||||
EXPECT_NE(second.find("draws=3"), String::npos) << second;
|
||||
EXPECT_NE(second.find("frames=2"), String::npos) << second;
|
||||
}
|
||||
|
||||
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroDraws) {
|
||||
// FormatWindowLine is pure. It used to rewrite the window bases as a side effect of
|
||||
// formatting, so any second reader - a probe, a test, a second reporting channel -
|
||||
// silently zeroed the next window.
|
||||
TEST_F(PipeStatsTest, FormattingTwiceDoesNotConsumeTheWindow) {
|
||||
PS::AddCalls(PS::CallClass::Draws, 7);
|
||||
PS::OnPresent();
|
||||
const String line = PS::FormatSummaryLine();
|
||||
EXPECT_NE(line.find("acc/draw=0.00"), String::npos) << line;
|
||||
|
||||
const String first = PS::FormatWindowLine();
|
||||
const String second = PS::FormatWindowLine();
|
||||
EXPECT_EQ(first, second) << first << "\n" << second;
|
||||
EXPECT_NE(second.find("draws=7"), String::npos) << second;
|
||||
|
||||
// ...and advancing explicitly does close it.
|
||||
PS::AdvanceSummaryWindow();
|
||||
const String third = PS::FormatWindowLine();
|
||||
EXPECT_NE(third.find("draws=0"), String::npos) << third;
|
||||
}
|
||||
|
||||
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroDraws) {
|
||||
PS::AddCalls(PS::CallClass::AccessorCalls, 12);
|
||||
PS::OnPresent();
|
||||
const String line = PS::FormatWindowLine();
|
||||
// No draw in the window means there is no per-draw number - and "0.00" beside a
|
||||
// non-zero acc= would read as one.
|
||||
EXPECT_NE(line.find("acc/draw=n/a"), String::npos) << line;
|
||||
EXPECT_NE(line.find("acc=12"), String::npos) << line;
|
||||
}
|
||||
|
||||
// A window with no Present in it has no per-frame reading at all. This used to divide by
|
||||
// a faked 1 and print the window TOTALS under a "/f" label: a scenario slice that draws
|
||||
// 47 times and never presents reported 1,404,550 staged bytes as a per-frame figure,
|
||||
// which is a 47x overstatement of the SEG_STAGE sizing input this package exists to
|
||||
// produce.
|
||||
TEST_F(PipeStatsTest, SummaryLineSurvivesZeroFrames) {
|
||||
PS::AddCalls(PS::CallClass::Draws, 47);
|
||||
PS::AddBytes(PS::ByteClass::StageBuffer, 1404550);
|
||||
|
||||
const String line = PS::FormatWindowLine();
|
||||
EXPECT_EQ(PS::FrameCount(), 0u);
|
||||
EXPECT_NE(line.find("window=0"), String::npos) << line;
|
||||
EXPECT_NE(line.find("draws/f=n/a"), String::npos) << line;
|
||||
// The bracket is relabelled rather than divided: totals, and marked as totals.
|
||||
EXPECT_EQ(line.find("bytes/f["), String::npos) << line;
|
||||
EXPECT_NE(line.find("bytes[buf=1404550"), String::npos) << line;
|
||||
}
|
||||
|
||||
TEST_F(PipeStatsTest, JsonDumpNamesEveryCounter) {
|
||||
@@ -191,6 +248,7 @@ namespace {
|
||||
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageUboNamed), "stage-ubo-named");
|
||||
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageVertexClient), "stage-vertex-client");
|
||||
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndexClient), "stage-index-client");
|
||||
EXPECT_STREQ(PS::NameOf(PS::ByteClass::StageIndirectCmd), "stage-indirect-cmd");
|
||||
EXPECT_STREQ(PS::NameOf(PS::ByteClass::PersistentMapPush), "persistent-map-push");
|
||||
EXPECT_STREQ(PS::NameOf(PS::ByteClass::ResidualValueBlock), "residual-value-block");
|
||||
EXPECT_STREQ(PS::NameOf(PS::Gate::EsprytRenderState), "espryt-render-state");
|
||||
|
||||
@@ -15,42 +15,69 @@
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// 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 DirectGLES Managers.cpp: RespecifyStorageNow's glBufferData,
|
||||
// FlushPendingRangesNow's three shapes (map-write, glBufferSubData,
|
||||
// upload-ring stage). Covers every byte Espryt hands the driver for
|
||||
// a buffer object's contents.
|
||||
// NOT covered: DirectVulkan's own buffer staging (its buffer bytes
|
||||
// reach the GPU through a persistent map the frontend already owns,
|
||||
// so there is no second copy to count) - see the note on
|
||||
// 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 DirectGLES Managers.cpp texture upload: the bytes of whichever of
|
||||
// stage-texture ESPRYT (Managers.cpp texture upload): the bytes of whichever of
|
||||
// the three upload shapes ran (rect list / union box / whole level).
|
||||
// NOT covered: the DirectVulkan texture staging path, and Espryt's
|
||||
// compressed-texture and readback paths.
|
||||
// stage-ubo-global DirectGLES.cpp default-uniform-block image, both the UBO-ring
|
||||
// memcpy and the glBufferSubData fallback.
|
||||
// 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. Espryt contributes
|
||||
// nothing by construction (D-B8).
|
||||
// stage-vertex-client DirectGLES BackendVertexArrayObject::SyncClientSideAttributesFor-
|
||||
// DrawArrays, both the Float64-narrowing and the verbatim shapes.
|
||||
// NOT covered: the DirectVulkan converted-vertex-stream cache.
|
||||
// stage-index-client DirectGLES index rewriting (the primitive-restart substitution
|
||||
// buffer).
|
||||
// NOT covered: DirectVulkan's index staging.
|
||||
// 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 TrySetupDrawFastPath's
|
||||
// caller-visible entry. A dispatch is not a draw and is not counted.
|
||||
// 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. Covered: PrepareForDraw's own reads,
|
||||
// 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:
|
||||
@@ -59,7 +86,7 @@
|
||||
// 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-* DirectGLES texture upload, per (target, level) emission.
|
||||
// texture-* Per (target, level) emission, both backends.
|
||||
//
|
||||
// Gates: the six of section 2.3.1, each counted exactly once per probe.
|
||||
//
|
||||
@@ -122,9 +149,24 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
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", "persistent-map-push", "residual-value-block",
|
||||
"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",
|
||||
};
|
||||
const char* const kCallClassNames[kCallClassCount] = {
|
||||
"draws", "accessor-calls", "tex-upload-emissions", "tex-upload-box", "tex-upload-rect",
|
||||
@@ -134,19 +176,57 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
"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.
|
||||
const char* const kByteClassShort[kByteClassCount] = {"buf", "tex", "ubog", "ubon",
|
||||
"vtxc", "idxc", "pmap", "resid"};
|
||||
const char* const kByteClassShort[kByteClassCount] = {"buf", "tex", "ubog", "ubon", "vtxc",
|
||||
"idxc", "icmd", "pmap", "resid"};
|
||||
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 = FormatSummaryLine();
|
||||
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() {
|
||||
@@ -170,7 +250,7 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
} // namespace
|
||||
|
||||
void Init() {
|
||||
ResetForTesting();
|
||||
ResetCounters();
|
||||
g_shutdownDone = false;
|
||||
g_pipeStatsEnabled = MG_Config::Features.PipeStats;
|
||||
if (g_pipeStatsEnabled) {
|
||||
@@ -218,7 +298,13 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
void OnPresent() {
|
||||
#ifdef TRACY_ENABLE
|
||||
// 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.
|
||||
// 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.
|
||||
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
||||
TracyPlot(kByteClassNames[i], static_cast<Int64>(Read(g_frameBytes[i])));
|
||||
}
|
||||
@@ -226,7 +312,8 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
TracyPlot(kCallClassNames[i], static_cast<Int64>(Read(g_frameCalls[i])));
|
||||
}
|
||||
for (Uint32 i = 0; i < kGateCount; ++i) {
|
||||
TracyPlot(kGateNames[i], static_cast<Int64>(Read(g_frameGateMiss[i])));
|
||||
TracyPlot(kGateHitPlotNames[i], static_cast<Int64>(Read(g_frameGateHit[i])));
|
||||
TracyPlot(kGateMissPlotNames[i], static_cast<Int64>(Read(g_frameGateMiss[i])));
|
||||
}
|
||||
#endif
|
||||
for (Uint32 i = 0; i < kByteClassCount; ++i) {
|
||||
@@ -260,12 +347,16 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
const char* NameOf(CallClass callClass) { return kCallClassNames[static_cast<Uint32>(callClass)]; }
|
||||
const char* NameOf(Gate gate) { return kGateNames[static_cast<Uint32>(gate)]; }
|
||||
|
||||
String FormatSummaryLine() {
|
||||
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;
|
||||
const Uint64 divisorFrames = windowFrames == 0 ? 1 : windowFrames;
|
||||
// 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) {
|
||||
@@ -289,21 +380,21 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
line += " frames=" + std::to_string(frames);
|
||||
line += " window=" + std::to_string(windowFrames);
|
||||
line += " draws=" + std::to_string(draws);
|
||||
line += " draws/f=" + std::to_string(draws / divisorFrames);
|
||||
line += " draws/f=" + FormatFixed2(draws, windowFrames);
|
||||
line += " acc=" + std::to_string(accessorCalls);
|
||||
// Two decimals without <iomanip>: the per-draw accessor count is the number section
|
||||
// 2.3.1 wants to an integer's worth of precision, and it is small (10-25).
|
||||
const Uint64 accPerDrawHundredths = draws == 0 ? 0 : (accessorCalls * 100 + draws / 2) / draws;
|
||||
line += " acc/draw=" + std::to_string(accPerDrawHundredths / 100) + "." +
|
||||
(accPerDrawHundredths % 100 < 10 ? "0" : "") + std::to_string(accPerDrawHundredths % 100);
|
||||
line += " bytes/f[";
|
||||
// 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 += std::to_string(bytes[i] / divisorFrames);
|
||||
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)]);
|
||||
@@ -321,7 +412,10 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
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]);
|
||||
}
|
||||
@@ -332,8 +426,7 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
g_windowBaseGateHit[i] = Read(g_totalGateHit[i]);
|
||||
g_windowBaseGateMiss[i] = Read(g_totalGateMiss[i]);
|
||||
}
|
||||
g_windowBaseFrames = frames;
|
||||
return line;
|
||||
g_windowBaseFrames = Read(g_frameCount);
|
||||
}
|
||||
|
||||
String FormatJson() {
|
||||
@@ -374,30 +467,6 @@ namespace MobileGL::MG_Util::PipeStats {
|
||||
|
||||
void SetEnabledForTesting(Bool enabled) { g_pipeStatsEnabled = enabled; }
|
||||
|
||||
void ResetForTesting() {
|
||||
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 ResetForTesting() { ResetCounters(); }
|
||||
|
||||
} // namespace MobileGL::MG_Util::PipeStats
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user