diff --git a/host/CMakeLists.txt b/host/CMakeLists.txt index e863bab..930d477 100644 --- a/host/CMakeLists.txt +++ b/host/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable(coop_host WIN32 src/injection_panel.cpp src/capture_panel.cpp src/audio_panel.cpp + src/ui/app_chrome.cpp src/input/xinput_source.cpp src/inject/process_list.cpp src/inject/injector.cpp diff --git a/host/src/capture_panel.cpp b/host/src/capture_panel.cpp index b2480ef..f0a79b7 100644 --- a/host/src/capture_panel.cpp +++ b/host/src/capture_panel.cpp @@ -46,7 +46,6 @@ void CapturePanel::draw_ui() { ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Capturing %ux%u", capture_.frame_width(), capture_.frame_height()); - ImGui::Text("Render: %.1f FPS", ImGui::GetIO().Framerate); } ImGui::End(); diff --git a/host/src/debug_overlay.cpp b/host/src/debug_overlay.cpp index 8e51ee2..905d8d5 100644 --- a/host/src/debug_overlay.cpp +++ b/host/src/debug_overlay.cpp @@ -65,7 +65,6 @@ void draw_debug_overlay(const InputSource& input) ImGui::Begin("CoopAllTheThings - Phase 0 spike"); ImGui::Text("Input backend: %s", input.name()); - ImGui::Text("%.1f FPS (%.2f ms)", ImGui::GetIO().Framerate, 1000.0f / ImGui::GetIO().Framerate); ImGui::TextDisabled("This window is what Remote Play Together captures."); ImGui::TextDisabled("F1: hide overlay (clean mirror) Esc: quit"); ImGui::Separator(); diff --git a/host/src/main.cpp b/host/src/main.cpp index a3aefd1..c405bca 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -20,6 +20,7 @@ #include "imgui_layer.hpp" #include "injection_panel.hpp" #include "input/xinput_source.hpp" +#include "ui/app_chrome.hpp" namespace { @@ -76,6 +77,8 @@ int run() // Play Together; the pipelines keep running underneath either way. bool show_overlay = true; double overlay_hidden_at = 0.0; + coop::UiState ui; + coop::FrameStats stats; while (window.pump_messages()) { @@ -86,6 +89,7 @@ int run() audio.set_target(game); imgui.begin_frame(); + stats.tick(ImGui::GetIO().DeltaTime * 1000.0f); if (ImGui::IsKeyPressed(ImGuiKey_F1, false)) { @@ -98,10 +102,23 @@ int run() if (show_overlay) { - coop::draw_debug_overlay(*input); - injection.draw(); - audio.draw_ui(injection.hook_status()); - capture.draw_ui(); + coop::draw_main_menu_bar(ui, stats); + if (ui.show_controllers) + { + coop::draw_debug_overlay(*input); + } + if (ui.show_injection) + { + injection.draw(); + } + if (ui.show_audio) + { + audio.draw_ui(injection.hook_status()); + } + if (ui.show_video) + { + capture.draw_ui(); + } } else { diff --git a/host/src/ui/app_chrome.cpp b/host/src/ui/app_chrome.cpp new file mode 100644 index 0000000..46ef490 --- /dev/null +++ b/host/src/ui/app_chrome.cpp @@ -0,0 +1,63 @@ +#include "ui/app_chrome.hpp" + +#include + +#include "imgui.h" + +namespace coop +{ + +float draw_main_menu_bar(UiState& ui, const FrameStats& stats) +{ + float height = 0.0f; + if (!ImGui::BeginMainMenuBar()) + { + return height; + } + + ImGui::TextUnformatted("CoopAllTheThings"); + ImGui::Separator(); + + if (ImGui::BeginMenu("View")) + { + ImGui::MenuItem("Controllers", nullptr, &ui.show_controllers); + ImGui::MenuItem("Injection", nullptr, &ui.show_injection); + ImGui::MenuItem("Video mirror", nullptr, &ui.show_video); + ImGui::MenuItem("Audio mirror", nullptr, &ui.show_audio); + ImGui::Separator(); + ImGui::MenuItem("Debug details", nullptr, &ui.debug_details); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Help")) + { + ImGui::TextDisabled("F1 hide/show this overlay"); + ImGui::TextDisabled("Esc quit"); + ImGui::Separator(); + ImGui::TextDisabled("This window is what Remote Play"); + ImGui::TextDisabled("Together captures."); + ImGui::EndMenu(); + } + + // Right-aligned performance readout: stable FPS with the frame-time spread + // (min/max over the last second) so stutter is visible at a glance. + char perf[96]; + std::snprintf(perf, sizeof(perf), "%.0f FPS %.2f ms (%.2f-%.2f)", stats.fps(), stats.avg_ms(), + stats.min_ms(), stats.max_ms()); + const float text_w = ImGui::CalcTextSize(perf).x; + ImGui::SameLine(ImGui::GetWindowWidth() - text_w - ImGui::GetStyle().FramePadding.x * 2.0f); + if (stats.max_ms() > 25.0f) // ~sub-40 FPS spike in the window + { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", perf); + } + else + { + ImGui::TextUnformatted(perf); + } + + height = ImGui::GetWindowHeight(); + ImGui::EndMainMenuBar(); + return height; +} + +} // namespace coop diff --git a/host/src/ui/app_chrome.hpp b/host/src/ui/app_chrome.hpp new file mode 100644 index 0000000..04e08e6 --- /dev/null +++ b/host/src/ui/app_chrome.hpp @@ -0,0 +1,83 @@ +// Top-level overlay chrome shared across panels: which panels are visible, the +// global "show debug details" switch, rolling frame-timing stats, and the main +// menu bar that drives them. Keeping this in one place lets the individual panels +// stay focused on their own pipeline while presenting a consistent shell. +#pragma once + +namespace coop +{ + +// Visibility + verbosity shared by all panels. Panels read `debug_details` to +// gate verbose diagnostics; the main loop reads the per-panel flags to decide +// what to draw. +struct UiState +{ + bool show_controllers = true; + bool show_injection = true; + bool show_video = true; + bool show_audio = true; + bool debug_details = false; // off = general status; on = full diagnostics +}; + +// Rolling frame-timing over a ~1 s window, recomputed each window so the status +// bar can show a stable FPS plus the min/max frame time (jitter) underneath it. +class FrameStats +{ +public: + void tick(float dt_ms) + { + if (dt_ms < cur_min_) + { + cur_min_ = dt_ms; + } + if (dt_ms > cur_max_) + { + cur_max_ = dt_ms; + } + accum_ms_ += dt_ms; + ++frames_; + if (accum_ms_ >= 1000.0f && frames_ > 0) + { + avg_ms_ = accum_ms_ / static_cast(frames_); + min_ms_ = cur_min_; + max_ms_ = cur_max_; + cur_min_ = 1.0e9f; + cur_max_ = 0.0f; + accum_ms_ = 0.0f; + frames_ = 0; + } + } + + [[nodiscard]] float avg_ms() const + { + return avg_ms_; + } + [[nodiscard]] float min_ms() const + { + return min_ms_; + } + [[nodiscard]] float max_ms() const + { + return max_ms_; + } + [[nodiscard]] float fps() const + { + return avg_ms_ > 0.0f ? 1000.0f / avg_ms_ : 0.0f; + } + +private: + float accum_ms_ = 0.0f; + int frames_ = 0; + float cur_min_ = 1.0e9f; + float cur_max_ = 0.0f; + float avg_ms_ = 0.0f; + float min_ms_ = 0.0f; + float max_ms_ = 0.0f; +}; + +// Draw the main menu bar (app name, View menu of panel toggles + debug switch, +// and a right-aligned FPS readout). Returns the menu bar height so the caller can +// keep panels clear of it on first layout. +float draw_main_menu_bar(UiState& ui, const FrameStats& stats); + +} // namespace coop