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:
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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] = {};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user