[Feat] (DirectGLES, DirectVulkan): instrument the six memo gates, the staging byte paths and both Present hooks with the MGPipe counters

- The sites of plan B section 2.3.1, verified against dev@81b17c0b (the plan's own line
  numbers for DirectGLES drift by 4-9 lines; the DirectVulkan ones are exact):
  SyncRenderState is DirectGLES.cpp:1994 with the version read at :1998 and the
  early-out at :2007-2010 (plan says 2003 / 2007 / 2016-2018); SyncNeccessaryTextures
  at :1511 (plan :1520); CurrentUnitBindingsEpoch at :1412-1435 (plan :1418-1436);
  PrepareForDraw at :2907-2968 (plan :2916-2976); the global-UBO upload at :3355-3397
  (plan :3369-3392); TrySetupDrawFastPath :5994, GetOrCreatePipeline :4948-4993,
  ApplyDynamicDrawStateTail :5871-5893 and UniformManager ResolveUniformBufferPayload
  :2022/:2052 all as cited.
- Accessor counting is STATIC TALLIES at ten hot entry points, not a wrapper around the
  293 pGLContext-> sites: each instrumented function adds the number of accessor calls
  its own body made on the path taken. Reads inside callees, and every conditional read
  (the sRGB capability in SyncRenderState, the XFB probe and the version-gated parameter
  fetch in TrySetupDrawFastPath, the cull-mode/logic-op/tessellation reads in the
  pipeline payload builder) are excluded, so the number is a consistent LOWER bound. The
  full inventory of what is and is not counted is the header comment of PipeStats.cpp.
- The Magma fast-path gate is counted from SetupDraw, not from inside
  TrySetupDrawFastPath: that function has 27 decline returns and one success return, and
  counting at the caller is the only shape that cannot miss one.
- The texture upload counts the SHAPE (union box vs N-rect list) separately from the
  bytes, because SSIM is blind to the shape and the +6 ms/frame Mali regression of
  section 7.3 was a shape regression, not a byte one.
- Frame boundary: DirectGLES::Present after the ring upkeep, and DirectVulkan's backend
  Present rather than VulkanRenderer::Present - the latter has an early return for the
  no-usable-swapchain case, and a suspended frame is still a frame the counters close.
- Measured on lavapipe/llvmpipe with MOBILEGL_PIPE_STATS=1, GuiBatchScenario, 14 frames
  and 26 draws: Espryt 20.65 accessor calls per draw (gates ers 22/18, etl 71/9, eub
  71/9), Magma 15.54 (mfp 12/14, mpm 0/14, mdt 12/14). Both land inside the 10-25 band
  section 2.3.1 predicted and far below the 124/169 static counts, which is the
  correction that section was written to force.
This commit is contained in:
2026-09-05 20:16:49 -04:00
parent 42e0f47ebb
commit 7566a0b002
5 changed files with 209 additions and 0 deletions
@@ -15,6 +15,7 @@
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/MGToGL/DataTypeConverter.h>
#include <MG_Util/Converters/MGToGL/BufferEnumConverter.h>
@@ -779,6 +780,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
const void* initialData =
(size > 0 && bufferObject.HasDefinedContent()) ? bufferObject.MappedData() : nullptr;
g_GLESFuncs.glBufferData(TempBufferTarget, (GLsizeiptr)size, initialData, usage);
if (MG_Util::PipeStats::Enabled() && initialData != nullptr) {
// An ORPHANING respecify passes NULL and moves nothing, which is exactly
// why the test is on initialData rather than on size.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
resource.storageSize = size;
resource.storageInitialized = true;
resource.pendingRespecify = false;
@@ -886,6 +893,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const SizeT start = std::min(range.start, end);
const SizeT size = end - start;
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
// Counted once per queued range, before the three delivery shapes
// below diverge: all three move exactly these bytes, and it is the
// byte count - not the shape - that sizes SEG_STAGE.
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
// The invalidating map's fast path is SHAPE-dependent on this Mali
// driver: a whole-buffer invalidation renames the store outright,
// and a large range gets fresh pages - but a small unaligned range
@@ -953,6 +967,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (write.offset >= limit) continue;
const SizeT size = std::min(write.bytes.size(), limit - write.offset);
if (size == 0) continue;
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageBuffer,
static_cast<Uint64>(size));
}
SizeT ringOffset = 0;
if (ringUsable && size <= kUploadRingMaxBytes &&
RingAllocate(g_uploadRing, size, ringOffset)) {
@@ -2545,6 +2563,10 @@ 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()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(converted.size() * sizeof(Float)));
}
// GL ignores `normalized` for floating-point array types, so it is not
// forwarded here either.
g_GLESFuncs.glVertexAttribPointer(attribIndex, attrib.Size, GL_FLOAT, GL_FALSE,
@@ -2575,6 +2597,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
BufferImpl::BindBufferId(GL_ARRAY_BUFFER, bufferId);
g_GLESFuncs.glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(uploadSize), clientData,
GL_STREAM_DRAW);
if (MG_Util::PipeStats::Enabled()) {
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageVertexClient,
static_cast<Uint64>(uploadSize));
}
if (!attrib.IsInteger) {
const GLint glSize = attrib.IsBgra ? static_cast<GLint>(GL_BGRA) : attrib.Size;
@@ -4391,6 +4417,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (ringStaged) {
BufferImpl::BindPixelUnpackBufferId(BufferImpl::UnpackRingBufferId());
}
if (MG_Util::PipeStats::Enabled()) {
// One emission per (upload target, level) that ships texels;
// the switch below turns it into either one union-box job or
// dirtyRectCount rect jobs. The box/rect split is counted
// separately from the bytes on purpose: SSIM is blind to it
// and the +6 ms/frame Mali cliff was a shape regression, not
// a byte regression (plan section 7.3).
const Bool rectShape = subRectEligible && dirtyRectCount >= 2;
Uint64 shippedBytes = 0;
if (rectShape) {
for (SizeT r = 0; r < dirtyRectCount; ++r) {
const auto& rect = dirtyRects[r];
shippedBytes += static_cast<Uint64>(rect.hi.x() - rect.lo.x()) *
static_cast<Uint64>(rect.hi.y() - rect.lo.y()) *
static_cast<Uint64>(std::max(rect.hi.z() - rect.lo.z(), 1)) *
static_cast<Uint64>(bpp);
}
} else if (subRectEligible) {
shippedBytes = static_cast<Uint64>(regionSize.x()) *
static_cast<Uint64>(regionSize.y()) *
static_cast<Uint64>(std::max(regionSize.z(), 1)) *
static_cast<Uint64>(bpp);
} else {
shippedBytes = static_cast<Uint64>(byteSize);
}
MG_Util::PipeStats::AddBytes(MG_Util::PipeStats::ByteClass::StageTexture,
shippedBytes);
MG_Util::PipeStats::AddCalls(
MG_Util::PipeStats::CallClass::TextureUploadEmissions, 1);
MG_Util::PipeStats::AddCalls(
rectShape ? MG_Util::PipeStats::CallClass::TextureUploadRectEmissions
: MG_Util::PipeStats::CallClass::TextureUploadBoxEmissions,
1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::TextureUploadJobs,
rectShape ? static_cast<Uint64>(dirtyRectCount) : 1u);
}
switch (MapToBackendTextureTarget(stateTextureObject->GetTarget())) {
case TextureTarget::Texture2D:
case TextureTarget::TextureCubeMap: