Generalize UI: general status by default, diagnostics behind Debug details

Rework the spike-era debug readouts into general-purpose status, with the
verbose diagnostics gated behind the menu bar's "Debug details" switch:

- Controllers (was "Phase 0 spike"): always shows slot/source + live
  buttons; the raw stick/trigger numbers are debug-only.
- Injection hook status: general view is attached + focus spoof + a single
  "Game reading controller: N polls/s" summary; the per-slot poll table,
  focus-API counts, and input-path detection are debug-only.
- Audio: general view adds a "Buffered: N ms" health/latency proxy
  (AudioMirror now tracks buffered audio in both render paths) and keeps the
  render-stream count; the per-stream table is debug-only.

Default overlay is now clean general status; flip Debug details for the full
diagnostics. Completes the "generalize the UI" roadmap task. All tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 15:07:42 +02:00
parent b48cdd6515
commit d19974b17c
9 changed files with 79 additions and 31 deletions

View File

@@ -154,6 +154,7 @@ void AudioMirror::stop()
audio_ring_shm_.reset();
running_.store(false, std::memory_order_release);
source_.store(Source::None, std::memory_order_relaxed);
buffered_ms_.store(0, std::memory_order_relaxed);
pid_ = 0;
}
@@ -380,6 +381,8 @@ bool AudioMirror::run_hooked(AudioRingHeader* ring)
}
const UINT32 avail = render_frames - padding;
const std::uint32_t ring_bytes = audio_ring_available(*ring);
buffered_ms_.store(static_cast<unsigned>(ring_bytes / frame_bytes * 1000 / rate),
std::memory_order_relaxed);
if (!primed && ring_bytes >= prime_bytes)
{
primed = true;
@@ -571,6 +574,9 @@ void AudioMirror::run_loopback(DWORD pid)
continue;
}
const UINT32 avail = render_frames - padding;
buffered_ms_.store(
static_cast<unsigned>(ring.available() / frame_bytes * 1000 / fmt->nSamplesPerSec),
std::memory_order_relaxed);
if (!primed && ring.available() >= prime_bytes)
{
primed = true;

View File

@@ -61,6 +61,13 @@ public:
return channels_.load(std::memory_order_relaxed);
}
// Audio currently buffered between capture and the output device, in ms — a
// health/latency proxy (rises if the consumer can't keep up). 0 when stopped.
[[nodiscard]] unsigned buffered_ms() const
{
return buffered_ms_.load(std::memory_order_relaxed);
}
// Which capture path is active, for the UI's source indicator.
enum class Source
{
@@ -107,6 +114,7 @@ private:
std::atomic<Source> source_{Source::None};
std::atomic<unsigned> sample_rate_{0};
std::atomic<unsigned> channels_{0};
std::atomic<unsigned> buffered_ms_{0};
mutable std::mutex status_mutex_;
std::string status_;

View File

@@ -29,7 +29,7 @@ const char* format_tag_name(std::uint32_t tag)
} // namespace
void AudioPanel::draw_ui(const HookStatusView& status)
void AudioPanel::draw_ui(const HookStatusView& status, bool debug_details)
{
const bool have_target = target_ != nullptr && IsWindow(target_);
DWORD pid = 0;
@@ -76,6 +76,7 @@ void AudioPanel::draw_ui(const HookStatusView& status)
ImGui::SameLine();
ImGui::TextColored(hooked ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s",
mirror_.source_name());
ImGui::Text("Buffered: %u ms", mirror_.buffered_ms());
}
const std::string mirror_status = mirror_.status();
if (!mirror_status.empty())
@@ -90,10 +91,10 @@ void AudioPanel::draw_ui(const HookStatusView& status)
ImGui::TextDisabled("Game audio also plays locally (echo). Inject the hook to remove it.");
}
// --- Render-stream debug view -----------------------------------------
// --- Render-stream view -----------------------------------------------
// The hook counts every render stream the game creates, even with mirroring
// off. v1 captures only the first ("primary"); this makes multi-stream games
// obvious so we know if/when that needs revisiting.
// off. v1 captures only the first ("primary"); the count makes a multi-stream
// game obvious, and the per-stream table (debug details) shows why.
ImGui::Separator();
ImGui::Text("Render streams: %u", status.audio_streams_seen);
if (status.audio_streams_seen > kMaxAudioStreams)
@@ -102,6 +103,12 @@ void AudioPanel::draw_ui(const HookStatusView& status)
ImGui::TextDisabled("(showing first %u)", kMaxAudioStreams);
}
if (!debug_details)
{
ImGui::End();
return; // the per-stream table below is diagnostic detail
}
const std::uint32_t rows = std::min<std::uint32_t>(status.audio_streams_seen, kMaxAudioStreams);
if (rows > 0 &&
ImGui::BeginTable("audio_streams", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit))

View File

@@ -23,8 +23,9 @@ public:
target_ = target;
}
// `status` is the hook's back-channel, for the render-stream debug view.
void draw_ui(const HookStatusView& status);
// `status` is the hook's back-channel, for the render-stream view. With
// `debug_details` on, the per-stream table is shown.
void draw_ui(const HookStatusView& status, bool debug_details);
private:
HWND target_ = nullptr;

View File

@@ -21,7 +21,7 @@ constexpr ButtonBit kButtons[] = {
{0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"},
};
void draw_pad(int index, const PadInfo& pad)
void draw_pad(int index, const PadInfo& pad, bool debug_details)
{
ImGui::PushID(index);
if (!pad.connected)
@@ -31,11 +31,10 @@ void draw_pad(int index, const PadInfo& pad)
return;
}
ImGui::Text("Slot %d [%s]", index, pad.source.c_str());
ImGui::Text("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger);
ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly, pad.state.thumb_rx,
pad.state.thumb_ry);
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Slot %d [%s]", index, pad.source.c_str());
// Live button state is useful general feedback (is the guest pressing
// anything?); the raw axis/trigger numbers are debug detail.
bool first = true;
ImGui::TextUnformatted("Buttons: ");
for (const ButtonBit& b : kButtons)
@@ -52,17 +51,24 @@ void draw_pad(int index, const PadInfo& pad)
ImGui::SameLine();
ImGui::TextDisabled("(none)");
}
if (debug_details)
{
ImGui::Text("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger);
ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly,
pad.state.thumb_rx, pad.state.thumb_ry);
}
ImGui::Separator();
ImGui::PopID();
}
} // namespace
void draw_debug_overlay(const InputSource& input)
void draw_controllers_panel(const InputSource& input, bool debug_details)
{
ImGui::SetNextWindowPos(ImVec2(24, 24), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(24, 40), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_FirstUseEver);
ImGui::Begin("CoopAllTheThings - Phase 0 spike");
ImGui::Begin("Controllers");
ImGui::Text("Input backend: %s", input.name());
ImGui::TextDisabled("This window is what Remote Play Together captures.");
@@ -72,7 +78,7 @@ void draw_debug_overlay(const InputSource& input)
const auto& pads = input.pads();
for (int i = 0; i < static_cast<int>(pads.size()); ++i)
{
draw_pad(i, pads[i]);
draw_pad(i, pads[i], debug_details);
}
ImGui::End();

View File

@@ -5,9 +5,9 @@
namespace coop
{
// Phase 0 overlay: confirms the host is alive and, crucially, shows which
// controllers are visible -- this is how we verify Remote Play Together is
// routing a guest's gamepad into our window.
void draw_debug_overlay(const InputSource& input);
// Controllers panel: shows which controllers are visible to the host -- how we
// verify Remote Play Together is routing a guest's gamepad into our window. With
// `debug_details` on it also shows the full per-axis / per-button breakdown.
void draw_controllers_panel(const InputSource& input, bool debug_details);
} // namespace coop

View File

@@ -139,7 +139,7 @@ void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
server_.publish(synthetic);
}
void InjectionPanel::draw_hook_status()
void InjectionPanel::draw_hook_status(bool debug_details)
{
if (!server_.running())
{
@@ -170,13 +170,31 @@ void InjectionPanel::draw_hook_status()
return;
}
ImGui::TextColored(kGreen, "Attached (game pid %u, hwnd 0x%llX)", status.game_pid,
static_cast<unsigned long long>(status.game_hwnd));
ImGui::TextColored(kGreen, "Attached (game pid %u)", status.game_pid);
ImGui::TextColored(status.focus_spoof ? kGreen : kGrey, "Focus spoof: %s",
status.focus_spoof ? "active" : "inactive");
// Per-slot XInput polling: shows exactly which slots the game reads and how
// fast -- the requested visualization.
// General summary: is the game actually polling our pad, and how fast.
double total_rate = 0.0;
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
{
total_rate += state_rate_[i];
}
if (total_rate > 0.0)
{
ImGui::TextColored(kGreen, "Game reading controller: %.0f polls/s", total_rate);
}
else
{
ImGui::TextColored(kGrey, "Game reading controller: idle");
}
if (!debug_details)
{
return; // everything below is diagnostic detail
}
// Per-slot XInput polling: shows exactly which slots the game reads and how fast.
if (ImGui::BeginTable("slots", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Slot");
@@ -233,7 +251,7 @@ void InjectionPanel::draw_hook_status()
status.dinput_loaded ? "yes (could be foreground-gated)" : "no");
}
void InjectionPanel::draw()
void InjectionPanel::draw(bool debug_details)
{
ImGui::SetNextWindowPos(ImVec2(24, 360), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(420, 380), ImGuiCond_FirstUseEver);
@@ -300,7 +318,7 @@ void InjectionPanel::draw()
ImGui::TextDisabled("(ignores your controller)");
}
draw_hook_status();
draw_hook_status(debug_details);
ImGui::End();
}

View File

@@ -21,7 +21,9 @@ class InjectionPanel
public:
InjectionPanel();
void draw();
// `debug_details` shows the verbose hook diagnostics (per-slot poll table,
// focus-API counts, input-path detection); off shows a general summary.
void draw(bool debug_details);
// Forward the latest pad snapshot to the injected hook (if connected). When
// test-input mode is on, a synthetic pattern is sent instead of `pads`.
@@ -43,7 +45,7 @@ public:
private:
void refresh_processes();
void inject_selected();
void draw_hook_status();
void draw_hook_status(bool debug_details);
std::vector<ProcessEntry> processes_;
char filter_[128] = {};

View File

@@ -105,15 +105,15 @@ int run()
coop::draw_main_menu_bar(ui, stats);
if (ui.show_controllers)
{
coop::draw_debug_overlay(*input);
coop::draw_controllers_panel(*input, ui.debug_details);
}
if (ui.show_injection)
{
injection.draw();
injection.draw(ui.debug_details);
}
if (ui.show_audio)
{
audio.draw_ui(injection.hook_status());
audio.draw_ui(injection.hook_status(), ui.debug_details);
}
if (ui.show_video)
{