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();
|
audio_ring_shm_.reset();
|
||||||
running_.store(false, std::memory_order_release);
|
running_.store(false, std::memory_order_release);
|
||||||
source_.store(Source::None, std::memory_order_relaxed);
|
source_.store(Source::None, std::memory_order_relaxed);
|
||||||
|
buffered_ms_.store(0, std::memory_order_relaxed);
|
||||||
pid_ = 0;
|
pid_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,6 +381,8 @@ bool AudioMirror::run_hooked(AudioRingHeader* ring)
|
|||||||
}
|
}
|
||||||
const UINT32 avail = render_frames - padding;
|
const UINT32 avail = render_frames - padding;
|
||||||
const std::uint32_t ring_bytes = audio_ring_available(*ring);
|
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)
|
if (!primed && ring_bytes >= prime_bytes)
|
||||||
{
|
{
|
||||||
primed = true;
|
primed = true;
|
||||||
@@ -571,6 +574,9 @@ void AudioMirror::run_loopback(DWORD pid)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const UINT32 avail = render_frames - padding;
|
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)
|
if (!primed && ring.available() >= prime_bytes)
|
||||||
{
|
{
|
||||||
primed = true;
|
primed = true;
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ public:
|
|||||||
return channels_.load(std::memory_order_relaxed);
|
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.
|
// Which capture path is active, for the UI's source indicator.
|
||||||
enum class Source
|
enum class Source
|
||||||
{
|
{
|
||||||
@@ -107,6 +114,7 @@ private:
|
|||||||
std::atomic<Source> source_{Source::None};
|
std::atomic<Source> source_{Source::None};
|
||||||
std::atomic<unsigned> sample_rate_{0};
|
std::atomic<unsigned> sample_rate_{0};
|
||||||
std::atomic<unsigned> channels_{0};
|
std::atomic<unsigned> channels_{0};
|
||||||
|
std::atomic<unsigned> buffered_ms_{0};
|
||||||
|
|
||||||
mutable std::mutex status_mutex_;
|
mutable std::mutex status_mutex_;
|
||||||
std::string status_;
|
std::string status_;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const char* format_tag_name(std::uint32_t tag)
|
|||||||
|
|
||||||
} // namespace
|
} // 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_);
|
const bool have_target = target_ != nullptr && IsWindow(target_);
|
||||||
DWORD pid = 0;
|
DWORD pid = 0;
|
||||||
@@ -76,6 +76,7 @@ void AudioPanel::draw_ui(const HookStatusView& status)
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextColored(hooked ? ImVec4(0.4f, 1.0f, 0.4f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s",
|
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());
|
mirror_.source_name());
|
||||||
|
ImGui::Text("Buffered: %u ms", mirror_.buffered_ms());
|
||||||
}
|
}
|
||||||
const std::string mirror_status = mirror_.status();
|
const std::string mirror_status = mirror_.status();
|
||||||
if (!mirror_status.empty())
|
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.");
|
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
|
// The hook counts every render stream the game creates, even with mirroring
|
||||||
// off. v1 captures only the first ("primary"); this makes multi-stream games
|
// off. v1 captures only the first ("primary"); the count makes a multi-stream
|
||||||
// obvious so we know if/when that needs revisiting.
|
// game obvious, and the per-stream table (debug details) shows why.
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::Text("Render streams: %u", status.audio_streams_seen);
|
ImGui::Text("Render streams: %u", status.audio_streams_seen);
|
||||||
if (status.audio_streams_seen > kMaxAudioStreams)
|
if (status.audio_streams_seen > kMaxAudioStreams)
|
||||||
@@ -102,6 +103,12 @@ void AudioPanel::draw_ui(const HookStatusView& status)
|
|||||||
ImGui::TextDisabled("(showing first %u)", kMaxAudioStreams);
|
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);
|
const std::uint32_t rows = std::min<std::uint32_t>(status.audio_streams_seen, kMaxAudioStreams);
|
||||||
if (rows > 0 &&
|
if (rows > 0 &&
|
||||||
ImGui::BeginTable("audio_streams", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit))
|
ImGui::BeginTable("audio_streams", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit))
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ public:
|
|||||||
target_ = target;
|
target_ = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
// `status` is the hook's back-channel, for the render-stream debug view.
|
// `status` is the hook's back-channel, for the render-stream view. With
|
||||||
void draw_ui(const HookStatusView& status);
|
// `debug_details` on, the per-stream table is shown.
|
||||||
|
void draw_ui(const HookStatusView& status, bool debug_details);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
HWND target_ = nullptr;
|
HWND target_ = nullptr;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ constexpr ButtonBit kButtons[] = {
|
|||||||
{0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"},
|
{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);
|
ImGui::PushID(index);
|
||||||
if (!pad.connected)
|
if (!pad.connected)
|
||||||
@@ -31,11 +31,10 @@ void draw_pad(int index, const PadInfo& pad)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::Text("Slot %d [%s]", index, pad.source.c_str());
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "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);
|
|
||||||
|
|
||||||
|
// Live button state is useful general feedback (is the guest pressing
|
||||||
|
// anything?); the raw axis/trigger numbers are debug detail.
|
||||||
bool first = true;
|
bool first = true;
|
||||||
ImGui::TextUnformatted("Buttons: ");
|
ImGui::TextUnformatted("Buttons: ");
|
||||||
for (const ButtonBit& b : kButtons)
|
for (const ButtonBit& b : kButtons)
|
||||||
@@ -52,17 +51,24 @@ void draw_pad(int index, const PadInfo& pad)
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::TextDisabled("(none)");
|
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::Separator();
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // 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::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_FirstUseEver);
|
||||||
ImGui::Begin("CoopAllTheThings - Phase 0 spike");
|
ImGui::Begin("Controllers");
|
||||||
|
|
||||||
ImGui::Text("Input backend: %s", input.name());
|
ImGui::Text("Input backend: %s", input.name());
|
||||||
ImGui::TextDisabled("This window is what Remote Play Together captures.");
|
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();
|
const auto& pads = input.pads();
|
||||||
for (int i = 0; i < static_cast<int>(pads.size()); ++i)
|
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();
|
ImGui::End();
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
namespace coop
|
namespace coop
|
||||||
{
|
{
|
||||||
|
|
||||||
// Phase 0 overlay: confirms the host is alive and, crucially, shows which
|
// Controllers panel: shows which controllers are visible to the host -- how we
|
||||||
// controllers are visible -- this is how we verify Remote Play Together is
|
// verify Remote Play Together is routing a guest's gamepad into our window. With
|
||||||
// routing a guest's gamepad into our window.
|
// `debug_details` on it also shows the full per-axis / per-button breakdown.
|
||||||
void draw_debug_overlay(const InputSource& input);
|
void draw_controllers_panel(const InputSource& input, bool debug_details);
|
||||||
|
|
||||||
} // namespace coop
|
} // namespace coop
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
|
|||||||
server_.publish(synthetic);
|
server_.publish(synthetic);
|
||||||
}
|
}
|
||||||
|
|
||||||
void InjectionPanel::draw_hook_status()
|
void InjectionPanel::draw_hook_status(bool debug_details)
|
||||||
{
|
{
|
||||||
if (!server_.running())
|
if (!server_.running())
|
||||||
{
|
{
|
||||||
@@ -170,13 +170,31 @@ void InjectionPanel::draw_hook_status()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::TextColored(kGreen, "Attached (game pid %u, hwnd 0x%llX)", status.game_pid,
|
ImGui::TextColored(kGreen, "Attached (game pid %u)", status.game_pid);
|
||||||
static_cast<unsigned long long>(status.game_hwnd));
|
|
||||||
ImGui::TextColored(status.focus_spoof ? kGreen : kGrey, "Focus spoof: %s",
|
ImGui::TextColored(status.focus_spoof ? kGreen : kGrey, "Focus spoof: %s",
|
||||||
status.focus_spoof ? "active" : "inactive");
|
status.focus_spoof ? "active" : "inactive");
|
||||||
|
|
||||||
// Per-slot XInput polling: shows exactly which slots the game reads and how
|
// General summary: is the game actually polling our pad, and how fast.
|
||||||
// fast -- the requested visualization.
|
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))
|
if (ImGui::BeginTable("slots", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
|
||||||
{
|
{
|
||||||
ImGui::TableSetupColumn("Slot");
|
ImGui::TableSetupColumn("Slot");
|
||||||
@@ -233,7 +251,7 @@ void InjectionPanel::draw_hook_status()
|
|||||||
status.dinput_loaded ? "yes (could be foreground-gated)" : "no");
|
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::SetNextWindowPos(ImVec2(24, 360), ImGuiCond_FirstUseEver);
|
||||||
ImGui::SetNextWindowSize(ImVec2(420, 380), ImGuiCond_FirstUseEver);
|
ImGui::SetNextWindowSize(ImVec2(420, 380), ImGuiCond_FirstUseEver);
|
||||||
@@ -300,7 +318,7 @@ void InjectionPanel::draw()
|
|||||||
ImGui::TextDisabled("(ignores your controller)");
|
ImGui::TextDisabled("(ignores your controller)");
|
||||||
}
|
}
|
||||||
|
|
||||||
draw_hook_status();
|
draw_hook_status(debug_details);
|
||||||
|
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ class InjectionPanel
|
|||||||
public:
|
public:
|
||||||
InjectionPanel();
|
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
|
// 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`.
|
// test-input mode is on, a synthetic pattern is sent instead of `pads`.
|
||||||
@@ -43,7 +45,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void refresh_processes();
|
void refresh_processes();
|
||||||
void inject_selected();
|
void inject_selected();
|
||||||
void draw_hook_status();
|
void draw_hook_status(bool debug_details);
|
||||||
|
|
||||||
std::vector<ProcessEntry> processes_;
|
std::vector<ProcessEntry> processes_;
|
||||||
char filter_[128] = {};
|
char filter_[128] = {};
|
||||||
|
|||||||
@@ -105,15 +105,15 @@ int run()
|
|||||||
coop::draw_main_menu_bar(ui, stats);
|
coop::draw_main_menu_bar(ui, stats);
|
||||||
if (ui.show_controllers)
|
if (ui.show_controllers)
|
||||||
{
|
{
|
||||||
coop::draw_debug_overlay(*input);
|
coop::draw_controllers_panel(*input, ui.debug_details);
|
||||||
}
|
}
|
||||||
if (ui.show_injection)
|
if (ui.show_injection)
|
||||||
{
|
{
|
||||||
injection.draw();
|
injection.draw(ui.debug_details);
|
||||||
}
|
}
|
||||||
if (ui.show_audio)
|
if (ui.show_audio)
|
||||||
{
|
{
|
||||||
audio.draw_ui(injection.hook_status());
|
audio.draw_ui(injection.hook_status(), ui.debug_details);
|
||||||
}
|
}
|
||||||
if (ui.show_video)
|
if (ui.show_video)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user