Video mirror: frametime + FPS graphs with min/max/avg stats
The mirror renders into the host window, so the host's render frame timing is the mirror's performance. FrameStats now retains a ~2 s ring of frame-time samples; the Video mirror panel plots them as a frametime graph (0-33 ms scale) and an FPS graph (0-144 scale), each with an avg overlay, and prints avg/min/max for both frametime and FPS underneath. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
#include "capture_panel.hpp"
|
#include "capture_panel.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
namespace coop
|
namespace coop
|
||||||
@@ -11,9 +13,9 @@ bool CapturePanel::init(ID3D11Device* device)
|
|||||||
return renderer_.init(device);
|
return renderer_.init(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CapturePanel::draw_ui()
|
void CapturePanel::draw_ui(const FrameStats& stats)
|
||||||
{
|
{
|
||||||
ImGui::SetNextWindowPos(ImVec2(460, 24), ImGuiCond_FirstUseEver);
|
ImGui::SetNextWindowPos(ImVec2(460, 40), ImGuiCond_FirstUseEver);
|
||||||
ImGui::SetNextWindowSize(ImVec2(360, 0), ImGuiCond_FirstUseEver);
|
ImGui::SetNextWindowSize(ImVec2(360, 0), ImGuiCond_FirstUseEver);
|
||||||
ImGui::Begin("Video mirror");
|
ImGui::Begin("Video mirror");
|
||||||
|
|
||||||
@@ -48,9 +50,49 @@ void CapturePanel::draw_ui()
|
|||||||
capture_.frame_height());
|
capture_.frame_height());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draw_perf_graphs(stats);
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CapturePanel::draw_perf_graphs(const FrameStats& stats)
|
||||||
|
{
|
||||||
|
// The mirror renders into this window, so the host's render frame timing is
|
||||||
|
// the mirror's performance. Plot the recent history and summarise it.
|
||||||
|
ImGui::SeparatorText("Render performance");
|
||||||
|
|
||||||
|
float ms[FrameStats::kHistory] = {};
|
||||||
|
const int n = stats.copy_frame_ms(ms);
|
||||||
|
if (n == 0)
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("Gathering samples...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float fps[FrameStats::kHistory] = {};
|
||||||
|
for (int i = 0; i < n; ++i)
|
||||||
|
{
|
||||||
|
fps[i] = ms[i] > 0.0f ? 1000.0f / ms[i] : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float min_ms = 0.0f, max_ms = 0.0f, avg_ms = 0.0f;
|
||||||
|
stats.history_stats(min_ms, max_ms, avg_ms);
|
||||||
|
const float avg_fps = avg_ms > 0.0f ? 1000.0f / avg_ms : 0.0f;
|
||||||
|
const float min_fps = max_ms > 0.0f ? 1000.0f / max_ms : 0.0f; // slowest frame -> lowest FPS
|
||||||
|
const float max_fps = min_ms > 0.0f ? 1000.0f / min_ms : 0.0f;
|
||||||
|
|
||||||
|
char overlay[64];
|
||||||
|
std::snprintf(overlay, sizeof(overlay), "avg %.2f ms", avg_ms);
|
||||||
|
// Fixed 0..33 ms scale (30 FPS floor) so the line height is meaningful frame
|
||||||
|
// to frame rather than auto-rescaling.
|
||||||
|
ImGui::PlotLines("##frametime", ms, n, 0, overlay, 0.0f, 33.0f, ImVec2(-1.0f, 60.0f));
|
||||||
|
ImGui::Text("Frametime avg %.2f min %.2f max %.2f ms", avg_ms, min_ms, max_ms);
|
||||||
|
|
||||||
|
std::snprintf(overlay, sizeof(overlay), "avg %.0f FPS", avg_fps);
|
||||||
|
ImGui::PlotLines("##fps", fps, n, 0, overlay, 0.0f, 144.0f, ImVec2(-1.0f, 60.0f));
|
||||||
|
ImGui::Text("FPS avg %.0f min %.0f max %.0f", avg_fps, min_fps, max_fps);
|
||||||
|
}
|
||||||
|
|
||||||
void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h)
|
void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h)
|
||||||
{
|
{
|
||||||
if (capture_.running())
|
if (capture_.running())
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "capture/frame_renderer.hpp"
|
#include "capture/frame_renderer.hpp"
|
||||||
#include "capture/window_capture.hpp"
|
#include "capture/window_capture.hpp"
|
||||||
|
#include "ui/app_chrome.hpp"
|
||||||
|
|
||||||
namespace coop
|
namespace coop
|
||||||
{
|
{
|
||||||
@@ -24,12 +25,16 @@ public:
|
|||||||
target_ = target;
|
target_ = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
void draw_ui();
|
// `stats` are the host's render frame-timing, drawn as the mirror's
|
||||||
|
// frametime / FPS graphs (this window is what the mirror renders into).
|
||||||
|
void draw_ui(const FrameStats& stats);
|
||||||
|
|
||||||
// Render thread: draw the mirrored frame as the window background.
|
// Render thread: draw the mirrored frame as the window background.
|
||||||
void render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h);
|
void render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void draw_perf_graphs(const FrameStats& stats);
|
||||||
|
|
||||||
ID3D11Device* device_ = nullptr;
|
ID3D11Device* device_ = nullptr;
|
||||||
FrameRenderer renderer_;
|
FrameRenderer renderer_;
|
||||||
WindowCapture capture_;
|
WindowCapture capture_;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ int run()
|
|||||||
}
|
}
|
||||||
if (ui.show_video)
|
if (ui.show_video)
|
||||||
{
|
{
|
||||||
capture.draw_ui();
|
capture.draw_ui(stats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
// stay focused on their own pipeline while presenting a consistent shell.
|
// stay focused on their own pipeline while presenting a consistent shell.
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace coop
|
namespace coop
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -24,6 +26,9 @@ struct UiState
|
|||||||
class FrameStats
|
class FrameStats
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// Number of frame samples kept for the graphs (~2 s at 120 FPS).
|
||||||
|
static constexpr int kHistory = 240;
|
||||||
|
|
||||||
void tick(float dt_ms)
|
void tick(float dt_ms)
|
||||||
{
|
{
|
||||||
if (dt_ms < cur_min_)
|
if (dt_ms < cur_min_)
|
||||||
@@ -46,8 +51,16 @@ public:
|
|||||||
accum_ms_ = 0.0f;
|
accum_ms_ = 0.0f;
|
||||||
frames_ = 0;
|
frames_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
history_[hist_pos_] = dt_ms;
|
||||||
|
hist_pos_ = (hist_pos_ + 1) % kHistory;
|
||||||
|
if (hist_count_ < kHistory)
|
||||||
|
{
|
||||||
|
++hist_count_;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 1 s windowed aggregates (stable readout for the menu bar) ---------
|
||||||
[[nodiscard]] float avg_ms() const
|
[[nodiscard]] float avg_ms() const
|
||||||
{
|
{
|
||||||
return avg_ms_;
|
return avg_ms_;
|
||||||
@@ -65,6 +78,45 @@ public:
|
|||||||
return avg_ms_ > 0.0f ? 1000.0f / avg_ms_ : 0.0f;
|
return avg_ms_ > 0.0f ? 1000.0f / avg_ms_ : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Sample history (for graphs) ---------------------------------------
|
||||||
|
[[nodiscard]] int history_size() const
|
||||||
|
{
|
||||||
|
return hist_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the frame-time samples (ms) into `out` oldest-to-newest; `out` must
|
||||||
|
// hold at least kHistory floats. Returns the number written.
|
||||||
|
int copy_frame_ms(float* out) const
|
||||||
|
{
|
||||||
|
const int start = (hist_pos_ - hist_count_ + kHistory * 2) % kHistory;
|
||||||
|
for (int i = 0; i < hist_count_; ++i)
|
||||||
|
{
|
||||||
|
out[i] = history_[(start + i) % kHistory];
|
||||||
|
}
|
||||||
|
return hist_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// min / max / mean over the whole retained history (order-independent).
|
||||||
|
void history_stats(float& min_ms, float& max_ms, float& avg_ms) const
|
||||||
|
{
|
||||||
|
if (hist_count_ == 0)
|
||||||
|
{
|
||||||
|
min_ms = max_ms = avg_ms = 0.0f;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
float mn = 1.0e9f, mx = 0.0f, sum = 0.0f;
|
||||||
|
for (int i = 0; i < hist_count_; ++i)
|
||||||
|
{
|
||||||
|
const float v = history_[i];
|
||||||
|
mn = std::min(mn, v);
|
||||||
|
mx = std::max(mx, v);
|
||||||
|
sum += v;
|
||||||
|
}
|
||||||
|
min_ms = mn;
|
||||||
|
max_ms = mx;
|
||||||
|
avg_ms = sum / static_cast<float>(hist_count_);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
float accum_ms_ = 0.0f;
|
float accum_ms_ = 0.0f;
|
||||||
int frames_ = 0;
|
int frames_ = 0;
|
||||||
@@ -73,6 +125,10 @@ private:
|
|||||||
float avg_ms_ = 0.0f;
|
float avg_ms_ = 0.0f;
|
||||||
float min_ms_ = 0.0f;
|
float min_ms_ = 0.0f;
|
||||||
float max_ms_ = 0.0f;
|
float max_ms_ = 0.0f;
|
||||||
|
|
||||||
|
float history_[kHistory] = {};
|
||||||
|
int hist_pos_ = 0;
|
||||||
|
int hist_count_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw the main menu bar (app name, View menu of panel toggles + debug switch,
|
// Draw the main menu bar (app name, View menu of panel toggles + debug switch,
|
||||||
|
|||||||
Reference in New Issue
Block a user