diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 45375ee4..4ab2cf50 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -57,6 +57,7 @@ #include #endif +void P15CountFrame(); namespace MobileGL::MG_Impl::EGLImpl { namespace { // MOBILEGL_FPS_LOG=1 (on Android also the property debug.mobilegl.fps_log=1): one INFO line @@ -77,6 +78,7 @@ namespace MobileGL::MG_Impl::EGLImpl { } void NoteSwapForFrameRateLog() { + ::P15CountFrame(); if (!FrameRateLogEnabled()) return; using Clock = std::chrono::steady_clock; static Clock::time_point windowStart = Clock::now(); diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index cc00326f..dfb56a3b 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -23,6 +23,7 @@ #include "../Debug/GL_Debug.h" #include #include +#include "P15Count.inl" #define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) { @@ -53,12 +54,14 @@ } #else #define DECLARE_GL_FUNCTION_END(type, name, ...) \ + { static const int mglP15 = ::p15count::Reg(#name); ::p15count::Hit(mglP15); } \ MGLOG_D("Implementing function: %s(...)", __FUNCTION__); \ const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope; \ return MobileGL::MG_Impl::GLImpl::name(__VA_ARGS__); \ } #define DECLARE_GL_FUNCTION_END_NO_RETURN(type, name, ...) \ + { static const int mglP15 = ::p15count::Reg(#name); ::p15count::Hit(mglP15); } \ MGLOG_D("Implementing function: %s(...)", __FUNCTION__); \ const MobileGL::MG_Impl::EGLImpl::GLStreamScope mglStreamScope; \ MobileGL::MG_Impl::GLImpl::name(__VA_ARGS__); \ diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/P15Count.inl b/MobileGL/MG_Impl/GLImpl/Exporting/P15Count.inl new file mode 100644 index 00000000..1cb26c9a --- /dev/null +++ b/MobileGL/MG_Impl/GLImpl/Exporting/P15Count.inl @@ -0,0 +1,186 @@ +// P15 MEASUREMENT ONLY (apply to a scratch tree, never merge into product code): per-draw (VAO, program) pair statistics and the GL entries +// called between draws, to size the draw-packet cache. Armed by debug.mobilegl.p15count=1 or +// MOBILEGL_P15_COUNT=1. One log line per 600 presented frames. +#include +#include +#include +#include +#include +#include +#if defined(__ANDROID__) +#include +#endif +namespace p15count { + using MobileGL::Uint64; using MobileGL::Uint32; using MobileGL::SizeT; + // categories: 0 free (draw, BindVertexArray, float uniform, UseProgram), 1 int uniform / block binding, + // 2 texture/sampler/image bind, 3 buffer bind/data/vertex setup, 4 texture data/params, + // 5 render state, 6 framebuffer/clear/blit, 7 other + constexpr int kCats = 8; + struct Fn { const char* name; int cat; bool draw; bool strictFree; }; + inline std::vector& Fns() { static std::vector v; return v; } + inline bool On() { + static const bool on = [] { + const char* e = std::getenv("MOBILEGL_P15_COUNT"); + if (e && e[0] == '1') return true; +#if defined(__ANDROID__) + char p[PROP_VALUE_MAX] = {}; + if (__system_property_get("debug.mobilegl.p15count", p) > 0 && p[0] == '1') return true; +#endif + return false; + }(); + return on; + } + inline bool Pre(const char* n, const char* p) { return std::strncmp(n, p, std::strlen(p)) == 0; } + inline bool IsFloatUniform(const char* n) { + if (Pre(n, "Program")) n += 7; + if (!Pre(n, "Uniform")) return false; + n += 7; + if (Pre(n, "BlockBinding")) return false; + const SizeT l = std::strlen(n); + if (l == 0) return false; + // ...f / ...fv + return n[l - 1] == 'f' || (l >= 2 && n[l - 2] == 'f' && n[l - 1] == 'v'); + } + inline int Classify(const char* n, bool* draw, bool* strictFree) { + *draw = (Pre(n, "Draw") || Pre(n, "MultiDraw")) && !Pre(n, "DrawBuffer"); + const bool fu = IsFloatUniform(n); + *strictFree = *draw || std::strcmp(n, "BindVertexArray") == 0 || fu; + if (*strictFree || std::strcmp(n, "UseProgram") == 0) return 0; + if (Pre(n, "Uniform") || Pre(n, "ProgramUniform")) return 1; + static const char* tex[] = {"BindTexture", "ActiveTexture", "BindSampler", "BindTextureUnit", "BindImageTexture"}; + for (auto* t : tex) if (Pre(n, t)) return 2; + static const char* buf[] = {"BindBuffer", "BufferSubData", "BufferData", "NamedBuffer", "MapBuffer", "MapNamedBuffer", + "UnmapBuffer", "UnmapNamedBuffer", "FlushMapped", "VertexAttrib", "EnableVertexAttrib", + "DisableVertexAttrib", "BindVertexBuffer", "VertexBindingDivisor", "VertexArray", + "EnableVertexArrayAttrib", "DisableVertexArrayAttrib", "CopyBufferSubData", "ClearBuffer"}; + for (auto* t : buf) if (Pre(n, t)) return 3; + static const char* td[] = {"TexImage", "TexSubImage", "TexParameter", "TextureParameter", "TexStorage", "TextureStorage", + "TextureSubImage", "GenerateMipmap", "GenerateTextureMipmap", "CopyTex", "CompressedTex", "PixelStore"}; + for (auto* t : td) if (Pre(n, t)) return 4; + static const char* rs[] = {"Enable", "Disable", "Blend", "Depth", "ColorMask", "CullFace", "FrontFace", "Polygon", "Scissor", + "Viewport", "Stencil", "LineWidth", "PointSize", "SampleCoverage", "LogicOp", "ClipControl", + "PrimitiveRestart", "ProvokingVertex", "PatchParameter", "MinSampleShading", "SampleMask"}; + for (auto* t : rs) if (Pre(n, t)) return 5; + static const char* fb[] = {"BindFramebuffer", "Framebuffer", "NamedFramebuffer", "DrawBuffer", "ReadBuffer", "Clear", "Blit", + "ReadPixels", "BindRenderbuffer", "Renderbuffer", "InvalidateFramebuffer", "InvalidateSubFramebuffer"}; + for (auto* t : fb) if (Pre(n, t)) return 6; + return 7; + } + struct PairState { Uint64 lastFrame = ~0ull; Uint64 epoch[kCats] = {}; Uint64 strictEpoch = 0; Uint64 lastDraw = 0; }; + struct St { + Uint64 frame = 0, frames = 0, draws = 0, calls = 0; + Uint64 epoch[kCats] = {}; // epoch[k] bumps on any call with category > k (k = allowed max category) + Uint64 strictEpoch = 0; // bumps on anything outside {draw, BindVertexArray, float uniform} + Uint64 gapMask = 0; // categories since the previous draw + bool gapStrictDirty = false; + std::vector gapFns; std::vector gapStamp; Uint64 gapGen = 1; + std::vector fnGapDraws; // draws whose gap included fn + std::vector fnCalls; + std::unordered_map pairs; // key: vao lifetime * 2^24 ^ program lifetime + std::unordered_map pairsThisFrame; + Uint64 prevPair = ~0ull; + Uint64 sameAsPrev = 0, seenThisFrame = 0, seenPrevFrame = 0, newEver = 0, distinctFrameSum = 0; + Uint64 hit[kCats] = {}, strictHit = 0, prevDrawClean[kCats] = {}, prevDrawStrictClean = 0; + Uint64 maskHist[256] = {}; + Uint64 vaoZero = 0, progZero = 0; + }; + inline St& S() { static St s; return s; } + inline int Reg(const char* name) { + bool d, sf; const int c = Classify(name, &d, &sf); + Fns().push_back({name, c, d, sf}); + auto& s = S(); s.gapStamp.push_back(0); s.fnGapDraws.push_back(0); s.fnCalls.push_back(0); + return static_cast(Fns().size() - 1); + } + inline void Hit(int idx) { + if (!On()) return; + auto& s = S(); const Fn& f = Fns()[idx]; + ++s.calls; ++s.fnCalls[idx]; + if (!f.draw) { + for (int k = 0; k < f.cat; ++k) ++s.epoch[k]; + if (!f.strictFree) { ++s.strictEpoch; s.gapStrictDirty = true; } + s.gapMask |= 1ull << f.cat; + if (f.cat != 0 && s.gapStamp[idx] != s.gapGen) { s.gapStamp[idx] = s.gapGen; s.gapFns.push_back(idx); } + return; + } + // a draw: read the bound VAO and program before it runs + auto* ctx = MobileGL::MG_State::pGLContext.get(); + if (!ctx) return; + const auto& vao = ctx->GetBoundVertexArray(); + const auto& prog = ctx->GetCurrentProgram(); + const Uint64 v = vao ? vao->GetLifetimeId() : 0, p = prog ? prog->GetLifetimeId() : 0; + if (!v) ++s.vaoZero; + if (!p) ++s.progZero; + const Uint64 key = (v << 24) ^ p; + ++s.draws; + if (key == s.prevPair) ++s.sameAsPrev; + s.prevPair = key; + auto [fit, fnew] = s.pairsThisFrame.emplace(key, 1); + if (!fnew) { ++s.seenThisFrame; ++fit->second; } + auto [it, isNew] = s.pairs.try_emplace(key); + PairState& ps = it->second; + if (isNew) ++s.newEver; + else { + if (ps.lastFrame + 1 == s.frame || ps.lastFrame == s.frame) ++s.seenPrevFrame; + for (int k = 0; k < kCats; ++k) if (ps.epoch[k] == s.epoch[k]) ++s.hit[k]; + if (ps.strictEpoch == s.strictEpoch) ++s.strictHit; + } + for (int k = 0; k < kCats; ++k) ps.epoch[k] = s.epoch[k]; + ps.strictEpoch = s.strictEpoch; ps.lastFrame = s.frame; + // gap since previous draw + int maxCat = 0; for (int k = 0; k < kCats; ++k) if (s.gapMask >> k & 1) maxCat = k; + for (int k = 0; k < kCats; ++k) if (maxCat <= k) ++s.prevDrawClean[k]; + if (!s.gapStrictDirty) ++s.prevDrawStrictClean; + ++s.maskHist[s.gapMask & 255]; + for (Uint32 i : s.gapFns) ++s.fnGapDraws[i]; + s.gapFns.clear(); ++s.gapGen; s.gapMask = 0; s.gapStrictDirty = false; + } + inline void Frame() { + if (!On()) return; + auto& s = S(); + s.distinctFrameSum += s.pairsThisFrame.size(); + s.pairsThisFrame.clear(); + ++s.frame; ++s.frames; + static const Uint64 kWin = [] { const char* w = std::getenv("MOBILEGL_P15_COUNT_WINDOW"); return w ? std::strtoull(w, nullptr, 10) : 600ull; }(); + if (s.frames < kWin) return; + const double F = double(s.frames), D = s.draws ? double(s.draws) : 1.0; + MGLOG_I("P15CNT frames=%.0f draws/frame=%.1f glcalls/frame=%.1f distinctPairs/frame=%.1f pairsEver=%zu newPairs/frame=%.2f " + "sameAsPrev=%.1f%% seenThisFrame=%.1f%% seenPrevOrThisFrame=%.1f%% vao0=%.1f%% prog0=%.1f%%", + F, D / F, double(s.calls) / F, double(s.distinctFrameSum) / F, s.pairs.size(), double(s.newEver) / F, + 100.0 * s.sameAsPrev / D, 100.0 * s.seenThisFrame / D, 100.0 * s.seenPrevFrame / D, 100.0 * s.vaoZero / D, + 100.0 * s.progZero / D); + MGLOG_I("P15CNT strict(draw+BindVAO+floatUniform): pairEpochHit=%.1f%% prevDrawGapClean=%.1f%%", 100.0 * s.strictHit / D, + 100.0 * s.prevDrawStrictClean / D); + for (int k = 0; k < kCats; ++k) + MGLOG_I("P15CNT allow<=cat%d: pairEpochHit=%.1f%% prevDrawGapClean=%.1f%%", k, 100.0 * s.hit[k] / D, + 100.0 * s.prevDrawClean[k] / D); + // top gap masks + std::vector> mh; + for (int m = 0; m < 256; ++m) if (s.maskHist[m]) mh.push_back({s.maskHist[m], m}); + std::sort(mh.rbegin(), mh.rend()); + char line[1200]; int o = 0; + for (SizeT i = 0; i < mh.size() && i < 14 && o < 1100; ++i) o += std::snprintf(line + o, sizeof line - o, " %02x:%.1f%%", mh[i].second, 100.0 * mh[i].first / D); + MGLOG_I("P15CNT gapMasks(bit=cat):%s", line); + std::vector> fg; + for (SizeT i = 0; i < s.fnGapDraws.size(); ++i) if (s.fnGapDraws[i]) fg.push_back({s.fnGapDraws[i], int(i)}); + std::sort(fg.rbegin(), fg.rend()); + o = 0; + for (SizeT i = 0; i < fg.size() && i < 24 && o < 1100; ++i) + o += std::snprintf(line + o, sizeof line - o, " %s(c%d):%.1f%%", Fns()[fg[i].second].name, Fns()[fg[i].second].cat, 100.0 * fg[i].first / D); + MGLOG_I("P15CNT gapFns(%% of draws):%s", line); + std::vector> fc; + for (SizeT i = 0; i < s.fnCalls.size(); ++i) if (s.fnCalls[i]) fc.push_back({s.fnCalls[i], int(i)}); + std::sort(fc.rbegin(), fc.rend()); + o = 0; + for (SizeT i = 0; i < fc.size() && i < 30 && o < 1100; ++i) + o += std::snprintf(line + o, sizeof line - o, " %s:%.1f", Fns()[fc[i].second].name, double(fc[i].first) / F); + MGLOG_I("P15CNT calls/frame:%s", line); + // reset window counters (keep pair table and epochs) + s.frames = s.draws = s.calls = 0; s.sameAsPrev = s.seenThisFrame = s.seenPrevFrame = s.newEver = s.distinctFrameSum = 0; + s.strictHit = s.prevDrawStrictClean = s.vaoZero = s.progZero = 0; + for (int k = 0; k < kCats; ++k) s.hit[k] = s.prevDrawClean[k] = 0; + for (auto& m : s.maskHist) m = 0; + for (auto& x : s.fnGapDraws) x = 0; + for (auto& x : s.fnCalls) x = 0; + } +} +extern "C++" void P15CountFrame() { p15count::Frame(); }