diff --git a/host/src/capture_panel.cpp b/host/src/capture_panel.cpp index f0a79b7..25bddf1 100644 --- a/host/src/capture_panel.cpp +++ b/host/src/capture_panel.cpp @@ -1,5 +1,7 @@ #include "capture_panel.hpp" +#include + #include "imgui.h" namespace coop @@ -11,9 +13,9 @@ bool CapturePanel::init(ID3D11Device* 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::Begin("Video mirror"); @@ -48,9 +50,49 @@ void CapturePanel::draw_ui() capture_.frame_height()); } + draw_perf_graphs(stats); + 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) { if (capture_.running()) diff --git a/host/src/capture_panel.hpp b/host/src/capture_panel.hpp index 4d855eb..519fb83 100644 --- a/host/src/capture_panel.hpp +++ b/host/src/capture_panel.hpp @@ -9,6 +9,7 @@ #include "capture/frame_renderer.hpp" #include "capture/window_capture.hpp" +#include "ui/app_chrome.hpp" namespace coop { @@ -24,12 +25,16 @@ public: 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. void render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h); private: + void draw_perf_graphs(const FrameStats& stats); + ID3D11Device* device_ = nullptr; FrameRenderer renderer_; WindowCapture capture_; diff --git a/host/src/main.cpp b/host/src/main.cpp index 0acac95..9148173 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -117,7 +117,7 @@ int run() } if (ui.show_video) { - capture.draw_ui(); + capture.draw_ui(stats); } } else diff --git a/host/src/ui/app_chrome.hpp b/host/src/ui/app_chrome.hpp index 04e08e6..e849e6e 100644 --- a/host/src/ui/app_chrome.hpp +++ b/host/src/ui/app_chrome.hpp @@ -4,6 +4,8 @@ // stay focused on their own pipeline while presenting a consistent shell. #pragma once +#include + namespace coop { @@ -24,6 +26,9 @@ struct UiState class FrameStats { public: + // Number of frame samples kept for the graphs (~2 s at 120 FPS). + static constexpr int kHistory = 240; + void tick(float dt_ms) { if (dt_ms < cur_min_) @@ -46,8 +51,16 @@ public: accum_ms_ = 0.0f; 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 { return avg_ms_; @@ -65,6 +78,45 @@ public: 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(hist_count_); + } + private: float accum_ms_ = 0.0f; int frames_ = 0; @@ -73,6 +125,10 @@ private: float avg_ms_ = 0.0f; float min_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,