diff --git a/host/src/audio/audio_loopback.cpp b/host/src/audio/audio_loopback.cpp index d265456..709b19e 100644 --- a/host/src/audio/audio_loopback.cpp +++ b/host/src/audio/audio_loopback.cpp @@ -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(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(ring.available() / frame_bytes * 1000 / fmt->nSamplesPerSec), + std::memory_order_relaxed); if (!primed && ring.available() >= prime_bytes) { primed = true; diff --git a/host/src/audio/audio_loopback.hpp b/host/src/audio/audio_loopback.hpp index b13e6bc..dfcad8e 100644 --- a/host/src/audio/audio_loopback.hpp +++ b/host/src/audio/audio_loopback.hpp @@ -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::None}; std::atomic sample_rate_{0}; std::atomic channels_{0}; + std::atomic buffered_ms_{0}; mutable std::mutex status_mutex_; std::string status_; diff --git a/host/src/audio_panel.cpp b/host/src/audio_panel.cpp index 63b445b..915074f 100644 --- a/host/src/audio_panel.cpp +++ b/host/src/audio_panel.cpp @@ -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(status.audio_streams_seen, kMaxAudioStreams); if (rows > 0 && ImGui::BeginTable("audio_streams", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit)) diff --git a/host/src/audio_panel.hpp b/host/src/audio_panel.hpp index f914c61..ed9ceaa 100644 --- a/host/src/audio_panel.hpp +++ b/host/src/audio_panel.hpp @@ -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; diff --git a/host/src/debug_overlay.cpp b/host/src/debug_overlay.cpp index 905d8d5..c094067 100644 --- a/host/src/debug_overlay.cpp +++ b/host/src/debug_overlay.cpp @@ -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(pads.size()); ++i) { - draw_pad(i, pads[i]); + draw_pad(i, pads[i], debug_details); } ImGui::End(); diff --git a/host/src/debug_overlay.hpp b/host/src/debug_overlay.hpp index 35c6068..c40e46b 100644 --- a/host/src/debug_overlay.hpp +++ b/host/src/debug_overlay.hpp @@ -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 diff --git a/host/src/injection_panel.cpp b/host/src/injection_panel.cpp index 6126512..e627bac 100644 --- a/host/src/injection_panel.cpp +++ b/host/src/injection_panel.cpp @@ -139,7 +139,7 @@ void InjectionPanel::publish(const std::array& 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(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(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(); } diff --git a/host/src/injection_panel.hpp b/host/src/injection_panel.hpp index bebc33c..a62791b 100644 --- a/host/src/injection_panel.hpp +++ b/host/src/injection_panel.hpp @@ -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 processes_; char filter_[128] = {}; diff --git a/host/src/main.cpp b/host/src/main.cpp index c405bca..0acac95 100644 --- a/host/src/main.cpp +++ b/host/src/main.cpp @@ -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) {