From 9790f03d0bc6efd1a8e3ace5c2a1746fc8e5ed86 Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 28 Jul 2026 23:05:56 +0200 Subject: [PATCH] fps_metrics: add frametime coefficient of variation --- README.md | 2 +- data/MangoHud.conf | 5 +++-- src/fps_metrics.h | 40 +++++++++++++++++++++++++++++++++++++--- src/hud_elements.cpp | 8 +++++--- src/logging.cpp | 6 +----- src/overlay.cpp | 16 ++++++++++------ src/overlay.h | 3 ++- 7 files changed, 59 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ab48c9127c..d7ce530aa7 100644 --- a/README.md +++ b/README.md @@ -396,7 +396,7 @@ Parameters that are enabled by default have to be explicitly disabled. These (cu | `fps_only` | Show FPS only. ***Not meant to be used with other display params*** | | `fps_sampling_period=` | Time interval between two sampling points for gathering the FPS in milliseconds. Default is `500` | | `fps_value` | Choose the break points where `fps_color_change` changes colors between. E.g `60,144`, default is `30,60` | -| `fps_metrics` | Takes a list of decimal values or the value avg, e.g `avg,0.001` | +| `fps_metrics` | Takes a list of decimal percentile values, `avg`, or `cv`. CV is the population standard deviation divided by mean frametime, in percent; lower is steadier. E.g. `avg,0.001,cv` | | `reset_fps_metrics` | Reset fps metrics keybind, default is `Shift_R+F9` | | `fps_text` | Display custom text for engine name in front of FPS | | `frame_count` | Display frame count | diff --git a/data/MangoHud.conf b/data/MangoHud.conf index 276c6970cf..30dc8c1cf9 100644 --- a/data/MangoHud.conf +++ b/data/MangoHud.conf @@ -159,8 +159,9 @@ fps # fps_text="" frametime # frame_count -## fps_metrics takes a list of decimal values or the value avg -# fps_metrics=avg,0.01 +## fps_metrics takes a list of decimal percentile values, avg, or cv +## cv is the population standard deviation divided by mean frametime, in percent; lower is steadier +# fps_metrics=avg,0.01,cv ### Display GPU throttling status based on Power, current, temp or "other" ## Only shows if throttling is currently happening diff --git a/src/fps_metrics.h b/src/fps_metrics.h index 3d900d7598..961194ce98 100644 --- a/src/fps_metrics.h +++ b/src/fps_metrics.h @@ -10,12 +10,19 @@ #include #include #include +#include #include +enum class fps_metric_unit { + fps, + percent, +}; + struct metric_t { std::string name; float value; std::string display_name; + fps_metric_unit unit = fps_metric_unit::fps; }; class fpsMetrics { @@ -50,8 +57,27 @@ class fpsMetrics { if (frametimes.empty()) return; - std::vector sorted_values = frametimes; - std::sort(sorted_values.begin(), sorted_values.end(), std::greater()); + const bool has_cv = std::any_of(metrics.begin(), metrics.end(), [](const auto& metric) { + return metric.name == "CV"; + }); + double mean = 0.0; + double m2 = 0.0; + if (has_cv) { + mean = std::accumulate(frametimes.begin(), frametimes.end(), 0.0) / frametimes.size(); + for (const float frametime : frametimes) { + const double delta = frametime - mean; + m2 += delta * delta; + } + } + + const bool needs_sorted_values = std::any_of(metrics.begin(), metrics.end(), [](const auto& metric) { + return metric.name != "CV"; + }); + std::vector sorted_values; + if (needs_sorted_values) { + sorted_values = frametimes; + std::sort(sorted_values.begin(), sorted_values.end(), std::greater()); + } auto it = metrics.begin(); while (it != metrics.end()) { @@ -64,6 +90,9 @@ class fpsMetrics { float avg = 1000.f / (sum / sorted_values.size()); it->value = avg; + } else if (it->name == "CV") { + const double variance = m2 / frametimes.size(); + it->value = mean > 0.0 ? 100.0 * std::sqrt(variance) / mean : 0.0; } else { try { float val = std::stof(it->name); @@ -100,7 +129,12 @@ class fpsMetrics { for(char& c : val) { c = std::toupper(static_cast(c)); } - _metrics.push_back({val, 0.0f}); + metric_t metric {val, 0.0f, {}}; + if (val == "CV") { + metric.display_name = val; + metric.unit = fps_metric_unit::percent; + } + _metrics.push_back(metric); } return _metrics; } diff --git a/src/hud_elements.cpp b/src/hud_elements.cpp index 4dbf838acc..2f073679a3 100644 --- a/src/hud_elements.cpp +++ b/src/hud_elements.cpp @@ -1655,11 +1655,13 @@ void HudElements::fps_metrics(){ ImguiNextColumnFirstItem(); HUDElements.TextColored(HUDElements.colors.engine, "%s", metric.display_name.c_str()); ImguiNextColumnOrNewRow(); - right_aligned_text(HUDElements.colors.text, HUDElements.ralign_width, "%.0f", metric.value); + const bool is_percent = metric.unit == fps_metric_unit::percent; + right_aligned_text(HUDElements.colors.text, HUDElements.ralign_width, + is_percent ? "%.2f" : "%.0f", metric.value); ImGui::SameLine(0, 1.0f); - if(!HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_hide_fps_superscript]){ + if (is_percent || !HUDElements.params->enabled[OVERLAY_PARAM_ENABLED_hide_fps_superscript]){ ImGui::PushFont(HUDElements.sw_stats->font_small); - HUDElements.TextColored(HUDElements.colors.text, "FPS"); + HUDElements.TextColored(HUDElements.colors.text, "%s", is_percent ? "%" : "FPS"); ImGui::PopFont(); } ImguiNextColumnOrNewRow(); diff --git a/src/logging.cpp b/src/logging.cpp index f542fcfbf0..86b4a1aa0a 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -362,8 +362,6 @@ void Logger::calculate_benchmark_data(){ for (auto& point : m_log_array) fps_values.push_back(point.frametime); - benchmark.percentile_data.clear(); - std::vector metrics {"0.97", "avg", "0.01", "0.001"}; std::unique_ptr fpsmetrics; auto params = get_params(); @@ -371,9 +369,7 @@ void Logger::calculate_benchmark_data(){ metrics = params->fps_metrics; fpsmetrics = std::make_unique(metrics, fps_values); - auto metrics_copy = fpsmetrics->copy_metrics(); - for (auto& metric : metrics_copy) - benchmark.percentile_data.push_back({metric.display_name, metric.value}); + benchmark.metrics = fpsmetrics->copy_metrics(); fpsmetrics.reset(); } diff --git a/src/overlay.cpp b/src/overlay.cpp index 7f577fea3f..e28cc0a70d 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -257,7 +257,8 @@ void update_hud_info_with_frametime(struct swapchain_stats& sw_stats, const stru #endif frametime = frametime_ms; fps = double(1000 / frametime_ms); - if (fpsmetrics) fpsmetrics->update(frametime_ms); + if (fpsmetrics && sw_stats.last_present_time) + fpsmetrics->update(frametime_ms); if (elapsed >= real_params->fps_sampling_period) { if (!hw_update_thread) @@ -541,7 +542,7 @@ void render_mpris_metadata(const struct overlay_params& params, mutexed_metadata static void render_benchmark(swapchain_stats& data, const struct overlay_params& params, const ImVec2& window_size, unsigned height, Clock::time_point now){ // TODO, FIX LOG_DURATION FOR BENCHMARK - int benchHeight = (2 + benchmark.percentile_data.size()) * real_font_size.x + 10.0f + 58; + int benchHeight = (2 + benchmark.metrics.size()) * real_font_size.x + 10.0f + 58; ImGui::SetNextWindowSize(ImVec2(window_size.x, benchHeight), ImGuiCond_Always); if (height - (window_size.y + data.main_window_pos.y + 5) < benchHeight) ImGui::SetNextWindowPos(ImVec2(data.main_window_pos.x, data.main_window_pos.y - benchHeight - 5), ImGuiCond_Always); @@ -589,11 +590,14 @@ static void render_benchmark(swapchain_stats& data, const struct overlay_params& snprintf(duration, sizeof(duration), "Duration: %.1fs", std::chrono::duration(logger->last_log_end() - logger->last_log_begin()).count()); ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2 )- (ImGui::CalcTextSize(duration).x / 2)); ImGui::TextColored(ImVec4(1.0, 1.0, 1.0, alpha / params.background_alpha), "%s", duration); - for (auto& data_ : benchmark.percentile_data){ - char buffer[20]; - snprintf(buffer, sizeof(buffer), "%s %.1f", data_.first.c_str(), data_.second); + for (const auto& metric : benchmark.metrics){ + char buffer[32]; + if (metric.unit == fps_metric_unit::percent) + snprintf(buffer, sizeof(buffer), "%s %.2f%%", metric.display_name.c_str(), metric.value); + else + snprintf(buffer, sizeof(buffer), "%s %.1f", metric.display_name.c_str(), metric.value); ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2 )- (ImGui::CalcTextSize(buffer).x / 2)); - ImGui::TextColored(ImVec4(1.0, 1.0, 1.0, alpha / params.background_alpha), "%s %.1f", data_.first.c_str(), data_.second); + ImGui::TextColored(ImVec4(1.0, 1.0, 1.0, alpha / params.background_alpha), "%s", buffer); } float max = benchmark.fps_data.empty() ? 0.0f : *max_element(benchmark.fps_data.begin(), benchmark.fps_data.end()); diff --git a/src/overlay.h b/src/overlay.h index 7c8e6d5a98..c5e6ab3c9c 100644 --- a/src/overlay.h +++ b/src/overlay.h @@ -8,6 +8,7 @@ #include #include #include "imgui_internal.h" +#include "fps_metrics.h" #include "overlay_params.h" #include "hud_elements.h" @@ -80,7 +81,7 @@ struct swapchain_stats { struct benchmark_stats { float total; std::vector fps_data; - std::vector> percentile_data; + std::vector metrics; }; struct LOAD_DATA {