/** * Looking Glass * Copyright © 2017-2026 The Looking Glass Authors * https://looking-glass.io * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 2 of the License, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., 59 * Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include "interface/overlay.h" #include "cimgui.h" #include "cimplot.h" #include "../main.h" #include "../overlays.h" #include "common/array.h" #include "common/debug.h" #include "common/stringutils.h" #include "common/time.h" #include "overlay_utils.h" #include struct GraphState { bool show; struct ll * graphs; ImPlotContext * plotContext; ImPlotSpec * plotSpec; }; static struct GraphState gs = {0}; enum OverlayGraphType { OVERLAY_GRAPH_LINE, OVERLAY_GRAPH_FRAME_TIMING, }; #define TIMING_STATISTIC_COUNT 3 struct OverlayGraph { char * name; RingBuffer buffer; bool enabled; bool compact; enum OverlayGraphType type; float min; float max; float yScale[TIMING_STATISTIC_COUNT]; GraphFormatFn formatFn; }; static void graphFree(struct OverlayGraph * graph) { free(graph->name); free(graph); } static struct OverlayGraph * graphNew(const char * name, RingBuffer buffer) { if (!name || !*name) { DEBUG_ERROR("graph name must not be empty"); return NULL; } struct OverlayGraph * graph = calloc(1, sizeof(*graph)); if (!graph) { DEBUG_ERROR("out of memory"); return NULL; } graph->name = lg_strdup(name); if (!graph->name) { DEBUG_ERROR("out of memory"); graphFree(graph); return NULL; } graph->buffer = buffer; graph->enabled = true; return graph; } static GraphHandle graphPublish(struct OverlayGraph * graph) { if (!ll_push(gs.graphs, graph)) { graphFree(graph); return NULL; } return graph; } static void configCallback(void * udata, int * id) { igCheckbox("Show timing graphs", &gs.show); igSeparator(); if (igBeginTable("graphs_split", 2, 0, (ImVec2){}, 0)) { GraphHandle graph; ll_lock(gs.graphs); ll_forEachNL(gs.graphs, item, graph) { igTableNextColumn(); igPushID_Ptr(graph); igCheckbox(graph->name, &graph->enabled); igPopID(); } ll_unlock(gs.graphs); igEndTable(); } } static void showTimingKeybind(int sc, void * opaque) { gs.show ^= true; app_invalidateWindow(false); } static void graphs_earlyInit(void) { gs.graphs = ll_new(); } static bool graphs_init(void ** udata, const void * params) { gs.plotContext = ImPlot_CreateContext(); if (!gs.plotContext) { DEBUG_ERROR("Failed to create ImPlot context"); return false; } ImPlot_SetImGuiContext(igGetCurrentContext()); gs.plotSpec = ImPlotSpec_ImPlotSpec(); if (!gs.plotSpec) { DEBUG_ERROR("Failed to create ImPlot specification"); ImPlot_DestroyContext(gs.plotContext); gs.plotContext = NULL; return false; } app_overlayConfigRegister("Performance Metrics", configCallback, NULL); app_registerKeybind(KEY_T, showTimingKeybind, NULL, "Show frame timing information"); return true; } static void graphs_free(void * udata) { if (gs.graphs) { struct OverlayGraph * graph; while(ll_shift(gs.graphs, (void **)&graph)) graphFree(graph); ll_free(gs.graphs); gs.graphs = NULL; } if (gs.plotSpec) { ImPlotSpec_destroy(gs.plotSpec); gs.plotSpec = NULL; } if (gs.plotContext) { ImPlot_DestroyContext(gs.plotContext); gs.plotContext = NULL; } } struct BufferMetrics { float min; float max; float sum; float avg; float freq; float last; }; static bool rbCalcMetrics(int index, void * value_, void * udata_) { float * value = value_; struct BufferMetrics * udata = udata_; if (index == 0) { udata->min = *value; udata->max = *value; udata->sum = *value; udata->last = *value; return true; } if (udata->min > *value) udata->min = *value; if (udata->max < *value) udata->max = *value; udata->sum += *value; udata->last = *value; return true; } #define TIMING_PLOT_BUCKETS 100 #define TIMING_PLOT_WINDOW_NS 20000000000ULL #define TIMING_PLOT_BUCKET_NS \ (TIMING_PLOT_WINDOW_NS / TIMING_PLOT_BUCKETS) #define FRAME_TIMING_STAGE_COUNT OVERLAY_FRAME_TIMING_PRESENT static const char * const frameTimingLabels[FRAME_TIMING_STAGE_COUNT] = { "Capture", "Post", "Copy", "Ready", "Hold", "Transport", "Receive", "Provider Prep", "Import", "Dispatch", "Queue", "Prepare", "Setup", "Effects", "Desktop", "Compose", "Swap", }; struct TimingPlotData { uint64_t windowStart; uint64_t windowEnd; float stageMin[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; float stageMax[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; float stageSum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; unsigned stageSamples[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS]; unsigned samples[TIMING_PLOT_BUCKETS]; }; static float roundTimingScale(float value) { value = max(value, 1.0f); const float magnitude = powf(10.0f, floorf(log10f(value))); const float normalized = value / magnitude; const float rounded = normalized <= 1.0f ? 1.0f : normalized <= 1.5f ? 1.5f : normalized <= 2.0f ? 2.0f : normalized <= 3.0f ? 3.0f : normalized <= 5.0f ? 5.0f : normalized <= 7.5f ? 7.5f : 10.0f; return rounded * magnitude; } static float graphScale(struct OverlayGraph * graph, int scaleId, float peak) { float scale = graph->yScale[scaleId]; if (scale == 0.0f || peak > scale * 0.95f || peak < scale * 0.45f) scale = roundTimingScale(peak * 1.2f); graph->yScale[scaleId] = scale; return scale; } static float graphRowWeight(const struct OverlayGraph * graph) { if (graph->type == OVERLAY_GRAPH_FRAME_TIMING) return 3.0f; return graph->compact ? 0.5f : 1.0f; } static bool accumulateFrameTimingSample(int index, void * value_, void * udata_) { (void)index; struct TimingPlotData * data = udata_; const OverlayFrameTiming * timing = value_; if (timing->timestamp < data->windowStart) return true; if (timing->timestamp >= data->windowEnd) return false; const float values[FRAME_TIMING_STAGE_COUNT] = { timing->capture, timing->postProcess, timing->copy, timing->ready, timing->hold, timing->transport, timing->receive, timing->providerPrepare, timing->import, timing->dispatch, timing->queue, timing->prepare, timing->setup, timing->effects, timing->desktop, timing->compose, timing->swap, }; const uint64_t offset = timing->timestamp - data->windowStart; const int bucket = min(offset / TIMING_PLOT_BUCKET_NS, TIMING_PLOT_BUCKETS - 1); for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage) { if (!(timing->validMask & (1U << stage))) continue; const float value = values[stage]; if (!data->stageSamples[stage][bucket]) data->stageMin[stage][bucket] = data->stageMax[stage][bucket] = value; else { data->stageMin[stage][bucket] = min(data->stageMin[stage][bucket], value); data->stageMax[stage][bucket] = max(data->stageMax[stage][bucket], value); } data->stageSum[stage][bucket] += value; ++data->stageSamples[stage][bucket]; } ++data->samples[bucket]; return true; } static ImPlotFlags graphFlags(bool interactive, bool legend) { ImPlotFlags flags = legend ? ImPlotFlags_None : ImPlotFlags_NoLegend; if (!interactive) flags |= ImPlotFlags_NoInputs | ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect; return flags; } static void renderLineGraph(struct OverlayGraph * graph, bool interactive, ImVec2 size) { struct BufferMetrics metrics = {}; ringbuffer_forEach(graph->buffer, rbCalcMetrics, &metrics, false); const int count = ringbuffer_getCount(graph->buffer); if (metrics.sum > 0.0f && count > 0) { metrics.avg = metrics.sum / count; metrics.freq = 1000.0f / metrics.avg; } const char * description; if (graph->formatFn) description = graph->formatFn(graph->name, metrics.min, metrics.max, metrics.avg, metrics.freq, metrics.last); else { static char text[128]; snprintf(text, sizeof(text), "%s: min %.2f, max %.2f, avg %.2f ms / %.2f Hz", graph->name, metrics.min, metrics.max, metrics.avg, metrics.freq); description = text; } char title[256]; snprintf(title, sizeof(title), "%s###graph_%p", description, (void *)graph); if (!ImPlot_BeginPlot(title, size, graphFlags(interactive, false))) return; ImPlot_SetupAxes(NULL, "ms", ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_None); const float valueMax = graphScale(graph, 0, count > 0 ? metrics.max : graph->max); ImPlot_SetupAxesLimits(0.0, max(1, count - 1), graph->min, valueMax, ImPlotCond_Always); gs.plotSpec->Offset = ringbuffer_getStart(graph->buffer); gs.plotSpec->Stride = sizeof(float); gs.plotSpec->Flags = ImPlotLineFlags_None; ImPlot_PlotLine_FloatPtrInt(graph->name, ringbuffer_getValues(graph->buffer), count, 1.0, 0.0, *gs.plotSpec); ImPlot_EndPlot(); } static void renderTimingStatistic(struct OverlayGraph * graph, const char * statistic, int statisticId, const float values[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS], bool interactive, ImVec2 size) { float cumulative[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; float zero[TIMING_PLOT_BUCKETS]; float xValues[TIMING_PLOT_BUCKETS]; float peak = 0.0f; for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket) { const bool populated = isfinite(values[0][bucket]); xValues[bucket] = bucket; zero[bucket] = populated ? 0.0f : NAN; for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage) cumulative[stage][bucket] = populated ? values[stage][bucket] + (stage ? cumulative[stage - 1][bucket] : 0.0f) : NAN; if (populated) peak = max(peak, cumulative[FRAME_TIMING_STAGE_COUNT - 1][bucket]); } const float valueMax = graphScale(graph, statisticId, peak); char title[160]; snprintf(title, sizeof(title), "%s %s###graph_%p_%d", graph->name, statistic, (void *)graph, statisticId); if (!ImPlot_BeginPlot(title, size, graphFlags(interactive, true))) return; ImPlot_SetupAxes(NULL, "ms", ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_None); ImPlot_SetupAxesLimits(-0.5, TIMING_PLOT_BUCKETS - 0.5, 0.0, valueMax, ImPlotCond_Always); ImPlot_SetupLegend(ImPlotLocation_South, ImPlotLegendFlags_Outside | ImPlotLegendFlags_Horizontal); gs.plotSpec->Offset = 0; gs.plotSpec->Stride = sizeof(float); const int colorCount = ImPlot_GetColormapSize(ImPlotColormap_Paired); for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage) { const ImVec4 color = stage < colorCount ? ImPlot_GetColormapColor(stage, ImPlotColormap_Paired) : (ImVec4) {0.75f, 0.75f, 0.75f, 1.0f}; gs.plotSpec->FillColor = color; gs.plotSpec->FillAlpha = 0.35f; gs.plotSpec->LineWeight = 1.0f; gs.plotSpec->Flags = ImPlotShadedFlags_None; ImPlot_PlotShaded_FloatPtrFloatPtrFloatPtr( frameTimingLabels[stage], xValues, stage ? cumulative[stage - 1] : zero, cumulative[stage], TIMING_PLOT_BUCKETS, *gs.plotSpec); gs.plotSpec->LineColor = color; gs.plotSpec->LineWeight = 1.0f; gs.plotSpec->FillAlpha = 1.0f; gs.plotSpec->Flags = ImPlotLineFlags_None; ImPlot_PlotLine_FloatPtrInt(frameTimingLabels[stage], cumulative[stage], TIMING_PLOT_BUCKETS, 1.0, 0.0, *gs.plotSpec); } gs.plotSpec->LineColor = (ImVec4) {0.0f, 0.0f, 0.0f, -1.0f}; gs.plotSpec->FillColor = (ImVec4) {0.0f, 0.0f, 0.0f, -1.0f}; gs.plotSpec->LineWeight = 1.0f; ImPlot_EndPlot(); } static void renderFrameTimingGraph(struct OverlayGraph * graph, bool interactive, ImVec2 size) { struct TimingPlotData data = {}; data.windowEnd = nanotime() / TIMING_PLOT_BUCKET_NS * TIMING_PLOT_BUCKET_NS; data.windowStart = data.windowEnd > TIMING_PLOT_WINDOW_NS ? data.windowEnd - TIMING_PLOT_WINDOW_NS : 0; ringbuffer_forEach( graph->buffer, accumulateFrameTimingSample, &data, false); float minimum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; float maximum[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; float average[FRAME_TIMING_STAGE_COUNT][TIMING_PLOT_BUCKETS] = {}; for (int bucket = 0; bucket < TIMING_PLOT_BUCKETS; ++bucket) for (int stage = 0; stage < FRAME_TIMING_STAGE_COUNT; ++stage) { if (!data.samples[bucket]) { minimum[stage][bucket] = NAN; maximum[stage][bucket] = NAN; average[stage][bucket] = NAN; continue; } if (!data.stageSamples[stage][bucket]) continue; minimum[stage][bucket] = data.stageMin[stage][bucket]; maximum[stage][bucket] = data.stageMax[stage][bucket]; average[stage][bucket] = data.stageSum[stage][bucket] / data.stageSamples[stage][bucket]; } const float spacing = igGetStyle()->ItemSpacing.y; const float height = (size.y - spacing * 2.0f) / 3.0f; renderTimingStatistic(graph, "MINIMUM", 0, minimum, interactive, (ImVec2) {size.x, height}); renderTimingStatistic(graph, "MAXIMUM", 1, maximum, interactive, (ImVec2) {size.x, height}); renderTimingStatistic(graph, "AVERAGE", 2, average, interactive, (ImVec2) {size.x, height}); } static int graphs_render(void * udata, bool interactive, struct Rect * windowRects, int maxRects) { if (!gs.show) return 0; float fontSize = igGetFontSize(); GraphHandle graph; int graphCount = 0; float graphRows = 0.0f; ll_lock(gs.graphs); ll_forEachNL(gs.graphs, item, graph) { if (graph->enabled) { ++graphCount; graphRows += graphRowWeight(graph); } } if (!graphCount) { ll_unlock(gs.graphs); return 0; } ImVec2 pos = {0.0f, 0.0f}; igSetNextWindowBgAlpha(0.4f); igSetNextWindowPos(pos, ImGuiCond_FirstUseEver, pos); igSetNextWindowSize( (ImVec2){ 28.0f * fontSize, 7.0f * fontSize * graphRows }, ImGuiCond_FirstUseEver); ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav; if (!interactive) flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDecoration; igBegin("Performance Metrics", NULL, flags); ImVec2 winSize = igGetContentRegionAvail(); const float spacing = igGetStyle()->ItemSpacing.y; const float rowHeight = (winSize.y - spacing * (graphCount - 1)) / graphRows; for (int pass = 0; pass < 2; ++pass) { ll_forEachNL(gs.graphs, item, graph) { if (!graph->enabled || graph->compact != (pass == 1)) continue; igPushID_Ptr(graph); const float height = rowHeight * graphRowWeight(graph); if (graph->type == OVERLAY_GRAPH_FRAME_TIMING) renderFrameTimingGraph(graph, interactive, (ImVec2) {winSize.x, height}); else renderLineGraph(graph, interactive, (ImVec2) {winSize.x, height}); igPopID(); } } ll_unlock(gs.graphs); overlayGetImGuiRect(windowRects); igEnd(); return 1; } struct LG_OverlayOps LGOverlayGraphs = { .name = "Graphs", .earlyInit = graphs_earlyInit, .init = graphs_init, .free = graphs_free, .render = graphs_render }; GraphHandle overlayGraph_register(const char * name, RingBuffer buffer, float min, float max, GraphFormatFn formatFn) { struct OverlayGraph * graph = graphNew(name, buffer); if (!graph) return NULL; graph->type = OVERLAY_GRAPH_LINE; graph->min = min; graph->max = max; graph->formatFn = formatFn; return graphPublish(graph); } GraphHandle overlayGraph_registerFrameTiming(const char * name, RingBuffer buffer) { struct OverlayGraph * graph = graphNew(name, buffer); if (!graph) return NULL; graph->type = OVERLAY_GRAPH_FRAME_TIMING; return graphPublish(graph); } void overlayGraph_unregister(GraphHandle handle) { if (!gs.graphs || !handle) return; ll_removeData(gs.graphs, handle); graphFree(handle); if (gs.show) app_invalidateWindow(false); } void overlayGraph_setCompact(GraphHandle handle, bool compact) { if (handle) handle->compact = compact; } void overlayGraph_iterate(void (*callback)(GraphHandle handle, const char * name, bool * enabled, void * udata), void * udata) { GraphHandle graph; ll_lock(gs.graphs); ll_forEachNL(gs.graphs, item, graph) callback(graph, graph->name, &graph->enabled, udata); ll_unlock(gs.graphs); } void overlayGraph_invalidate(GraphHandle handle) { if (!gs.show) return; if (handle->enabled) app_invalidateWindow(false); }