Generalize UI: main menu bar with panel toggles + frame stats
Add a top menu bar (host/src/ui/app_chrome) as the overlay shell: - View menu toggles each panel (Controllers / Injection / Video / Audio) and a global "Debug details" switch (consumed by panels in a follow-up). - A right-aligned performance readout: stable 1 s FPS plus the frame-time spread (avg, min-max) so stutter is visible; it turns amber on a >25 ms spike. FrameStats keeps the rolling window. Consolidates the FPS that was duplicated in the controllers and video panels into this one readout. First structural step of the "generalize the UI" roadmap task; content rework (debug-vs-general split, renaming the spike-era readouts) follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ add_executable(coop_host WIN32
|
|||||||
src/injection_panel.cpp
|
src/injection_panel.cpp
|
||||||
src/capture_panel.cpp
|
src/capture_panel.cpp
|
||||||
src/audio_panel.cpp
|
src/audio_panel.cpp
|
||||||
|
src/ui/app_chrome.cpp
|
||||||
src/input/xinput_source.cpp
|
src/input/xinput_source.cpp
|
||||||
src/inject/process_list.cpp
|
src/inject/process_list.cpp
|
||||||
src/inject/injector.cpp
|
src/inject/injector.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(),
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Capturing %ux%u", capture_.frame_width(),
|
||||||
capture_.frame_height());
|
capture_.frame_height());
|
||||||
ImGui::Text("Render: %.1f FPS", ImGui::GetIO().Framerate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ void draw_debug_overlay(const InputSource& input)
|
|||||||
ImGui::Begin("CoopAllTheThings - Phase 0 spike");
|
ImGui::Begin("CoopAllTheThings - Phase 0 spike");
|
||||||
|
|
||||||
ImGui::Text("Input backend: %s", input.name());
|
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("This window is what Remote Play Together captures.");
|
||||||
ImGui::TextDisabled("F1: hide overlay (clean mirror) Esc: quit");
|
ImGui::TextDisabled("F1: hide overlay (clean mirror) Esc: quit");
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "imgui_layer.hpp"
|
#include "imgui_layer.hpp"
|
||||||
#include "injection_panel.hpp"
|
#include "injection_panel.hpp"
|
||||||
#include "input/xinput_source.hpp"
|
#include "input/xinput_source.hpp"
|
||||||
|
#include "ui/app_chrome.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -76,6 +77,8 @@ int run()
|
|||||||
// Play Together; the pipelines keep running underneath either way.
|
// Play Together; the pipelines keep running underneath either way.
|
||||||
bool show_overlay = true;
|
bool show_overlay = true;
|
||||||
double overlay_hidden_at = 0.0;
|
double overlay_hidden_at = 0.0;
|
||||||
|
coop::UiState ui;
|
||||||
|
coop::FrameStats stats;
|
||||||
|
|
||||||
while (window.pump_messages())
|
while (window.pump_messages())
|
||||||
{
|
{
|
||||||
@@ -86,6 +89,7 @@ int run()
|
|||||||
audio.set_target(game);
|
audio.set_target(game);
|
||||||
|
|
||||||
imgui.begin_frame();
|
imgui.begin_frame();
|
||||||
|
stats.tick(ImGui::GetIO().DeltaTime * 1000.0f);
|
||||||
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_F1, false))
|
if (ImGui::IsKeyPressed(ImGuiKey_F1, false))
|
||||||
{
|
{
|
||||||
@@ -97,12 +101,25 @@ int run()
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (show_overlay)
|
if (show_overlay)
|
||||||
|
{
|
||||||
|
coop::draw_main_menu_bar(ui, stats);
|
||||||
|
if (ui.show_controllers)
|
||||||
{
|
{
|
||||||
coop::draw_debug_overlay(*input);
|
coop::draw_debug_overlay(*input);
|
||||||
|
}
|
||||||
|
if (ui.show_injection)
|
||||||
|
{
|
||||||
injection.draw();
|
injection.draw();
|
||||||
|
}
|
||||||
|
if (ui.show_audio)
|
||||||
|
{
|
||||||
audio.draw_ui(injection.hook_status());
|
audio.draw_ui(injection.hook_status());
|
||||||
|
}
|
||||||
|
if (ui.show_video)
|
||||||
|
{
|
||||||
capture.draw_ui();
|
capture.draw_ui();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
draw_overlay_hidden_hint(ImGui::GetTime() - overlay_hidden_at);
|
draw_overlay_hidden_hint(ImGui::GetTime() - overlay_hidden_at);
|
||||||
|
|||||||
63
host/src/ui/app_chrome.cpp
Normal file
63
host/src/ui/app_chrome.cpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
#include "ui/app_chrome.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#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
|
||||||
83
host/src/ui/app_chrome.hpp
Normal file
83
host/src/ui/app_chrome.hpp
Normal file
@@ -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<float>(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
|
||||||
Reference in New Issue
Block a user