Move controller-poll info from Injection to Controllers panel
The per-slot XInput poll rates and the "game reading controller" summary describe the controller, not the injection mechanism, so move them to the Controllers panel (renamed from debug_overlay -> ControllersPanel, now a class that owns the poll-rate sampling). The panel now shows both directions: the guest pads the host receives from RPT, and what the injected game reads back. The Injection panel keeps the hook attach state, focus spoof, focus-API counts, and input-path diagnostics, and points to the Controllers panel for poll rates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@ add_executable(coop_host WIN32
|
|||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/d3d11_window.cpp
|
src/d3d11_window.cpp
|
||||||
src/imgui_layer.cpp
|
src/imgui_layer.cpp
|
||||||
src/debug_overlay.cpp
|
src/controllers_panel.cpp
|
||||||
src/injection_panel.cpp
|
src/injection_panel.cpp
|
||||||
src/capture_panel.cpp
|
src/capture_panel.cpp
|
||||||
src/audio_panel.cpp
|
src/audio_panel.cpp
|
||||||
|
|||||||
157
host/src/controllers_panel.cpp
Normal file
157
host/src/controllers_panel.cpp
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#include "controllers_panel.hpp"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
||||||
|
const ImVec4 kGrey(0.7f, 0.7f, 0.7f, 1.0f);
|
||||||
|
|
||||||
|
struct ButtonBit
|
||||||
|
{
|
||||||
|
std::uint16_t mask;
|
||||||
|
const char* label;
|
||||||
|
};
|
||||||
|
|
||||||
|
// XINPUT_GAMEPAD_* bit values (kept local so this file needn't include Xinput.h).
|
||||||
|
constexpr ButtonBit kButtons[] = {
|
||||||
|
{0x0001, "Up"}, {0x0002, "Down"}, {0x0004, "Left"}, {0x0008, "Right"}, {0x0010, "Start"},
|
||||||
|
{0x0020, "Back"}, {0x0040, "LS"}, {0x0080, "RS"}, {0x0100, "LB"}, {0x0200, "RB"},
|
||||||
|
{0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"},
|
||||||
|
};
|
||||||
|
|
||||||
|
void draw_pad(int index, const PadInfo& pad, bool debug_details)
|
||||||
|
{
|
||||||
|
ImGui::PushID(index);
|
||||||
|
if (!pad.connected)
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("Slot %d: disconnected", index);
|
||||||
|
ImGui::PopID();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::TextColored(kGreen, "Slot %d [%s]", index, pad.source.c_str());
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
ImGui::TextUnformatted("Buttons: ");
|
||||||
|
for (const ButtonBit& b : kButtons)
|
||||||
|
{
|
||||||
|
if ((pad.state.buttons & b.mask) != 0)
|
||||||
|
{
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::TextColored(kGreen, "%s%s", first ? "" : ", ", b.label);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first)
|
||||||
|
{
|
||||||
|
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 ControllersPanel::draw(const InputSource& input, const HookStatusView& status, bool debug_details)
|
||||||
|
{
|
||||||
|
ImGui::SetNextWindowPos(ImVec2(24, 40), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_FirstUseEver);
|
||||||
|
ImGui::Begin("Controllers");
|
||||||
|
|
||||||
|
ImGui::Text("Input backend: %s", input.name());
|
||||||
|
ImGui::TextDisabled("This window is what Remote Play Together captures.");
|
||||||
|
ImGui::TextDisabled("F1: hide overlay (clean mirror) Esc: quit");
|
||||||
|
|
||||||
|
// --- Guest pads the host receives from RPT -----------------------------
|
||||||
|
ImGui::SeparatorText("Incoming (host receives)");
|
||||||
|
const auto& pads = input.pads();
|
||||||
|
for (int i = 0; i < static_cast<int>(pads.size()); ++i)
|
||||||
|
{
|
||||||
|
draw_pad(i, pads[i], debug_details);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- What the injected game reads back via the XInput hook --------------
|
||||||
|
ImGui::SeparatorText("Game polling (hook reports)");
|
||||||
|
if (!status.attached)
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("Not injected (no XInput hook).");
|
||||||
|
ImGui::End();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the cumulative per-slot counters into rates every half second.
|
||||||
|
const double now = ImGui::GetTime();
|
||||||
|
if (now - last_sample_time_ >= 0.5)
|
||||||
|
{
|
||||||
|
const double dt = now - last_sample_time_;
|
||||||
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
||||||
|
{
|
||||||
|
const unsigned long long delta =
|
||||||
|
status.get_state[i] >= last_state_count_[i] ? status.get_state[i] - last_state_count_[i] : 0;
|
||||||
|
state_rate_[i] = dt > 0.0 ? static_cast<double>(delta) / dt : 0.0;
|
||||||
|
last_state_count_[i] = status.get_state[i];
|
||||||
|
}
|
||||||
|
last_sample_time_ = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 &&
|
||||||
|
ImGui::BeginTable("slots", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
|
||||||
|
{
|
||||||
|
ImGui::TableSetupColumn("Slot");
|
||||||
|
ImGui::TableSetupColumn("GetState/s");
|
||||||
|
ImGui::TableSetupColumn("GetState total");
|
||||||
|
ImGui::TableSetupColumn("GetCaps total");
|
||||||
|
ImGui::TableHeadersRow();
|
||||||
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
||||||
|
{
|
||||||
|
ImGui::TableNextRow();
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%d", i);
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
if (state_rate_[i] > 0.0)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(kGreen, "%.0f", state_rate_[i]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("0");
|
||||||
|
}
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%llu", static_cast<unsigned long long>(status.get_state[i]));
|
||||||
|
ImGui::TableNextColumn();
|
||||||
|
ImGui::Text("%llu", static_cast<unsigned long long>(status.get_caps[i]));
|
||||||
|
}
|
||||||
|
ImGui::EndTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
31
host/src/controllers_panel.hpp
Normal file
31
host/src/controllers_panel.hpp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Controllers panel: the controller view of the pipeline. Shows what the host
|
||||||
|
// receives from Remote Play Together (the guest pads, via the input backend) and
|
||||||
|
// what the injected game actually reads back (per-slot XInput poll counts from the
|
||||||
|
// hook). This is how we verify RPT routes a guest's gamepad in and the game polls
|
||||||
|
// it out. The per-axis breakdown and per-slot poll table are debug-only.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "coop/protocol.hpp"
|
||||||
|
#include "input/input_source.hpp"
|
||||||
|
#include "ipc/ipc_server.hpp"
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
class ControllersPanel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// `status` is the hook's back-channel (per-slot poll counters); `debug_details`
|
||||||
|
// reveals the raw axis values and the per-slot poll-rate table.
|
||||||
|
void draw(const InputSource& input, const HookStatusView& status, bool debug_details);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Sampled to turn the hook's cumulative per-slot counters into poll rates.
|
||||||
|
unsigned long long last_state_count_[kMaxPads] = {};
|
||||||
|
double state_rate_[kMaxPads] = {};
|
||||||
|
double last_sample_time_ = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#include "debug_overlay.hpp"
|
|
||||||
|
|
||||||
#include <imgui.h>
|
|
||||||
|
|
||||||
namespace coop
|
|
||||||
{
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
struct ButtonBit
|
|
||||||
{
|
|
||||||
std::uint16_t mask;
|
|
||||||
const char* label;
|
|
||||||
};
|
|
||||||
|
|
||||||
// XINPUT_GAMEPAD_* bit values (kept local so this file needn't include Xinput.h).
|
|
||||||
constexpr ButtonBit kButtons[] = {
|
|
||||||
{0x0001, "Up"}, {0x0002, "Down"}, {0x0004, "Left"}, {0x0008, "Right"}, {0x0010, "Start"},
|
|
||||||
{0x0020, "Back"}, {0x0040, "LS"}, {0x0080, "RS"}, {0x0100, "LB"}, {0x0200, "RB"},
|
|
||||||
{0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"},
|
|
||||||
};
|
|
||||||
|
|
||||||
void draw_pad(int index, const PadInfo& pad, bool debug_details)
|
|
||||||
{
|
|
||||||
ImGui::PushID(index);
|
|
||||||
if (!pad.connected)
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("Slot %d: disconnected", index);
|
|
||||||
ImGui::PopID();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if ((pad.state.buttons & b.mask) != 0)
|
|
||||||
{
|
|
||||||
ImGui::SameLine();
|
|
||||||
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%s%s", first ? "" : ", ", b.label);
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (first)
|
|
||||||
{
|
|
||||||
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_controllers_panel(const InputSource& input, bool debug_details)
|
|
||||||
{
|
|
||||||
ImGui::SetNextWindowPos(ImVec2(24, 40), ImGuiCond_FirstUseEver);
|
|
||||||
ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_FirstUseEver);
|
|
||||||
ImGui::Begin("Controllers");
|
|
||||||
|
|
||||||
ImGui::Text("Input backend: %s", input.name());
|
|
||||||
ImGui::TextDisabled("This window is what Remote Play Together captures.");
|
|
||||||
ImGui::TextDisabled("F1: hide overlay (clean mirror) Esc: quit");
|
|
||||||
ImGui::Separator();
|
|
||||||
|
|
||||||
const auto& pads = input.pads();
|
|
||||||
for (int i = 0; i < static_cast<int>(pads.size()); ++i)
|
|
||||||
{
|
|
||||||
draw_pad(i, pads[i], debug_details);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::End();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace coop
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "input/input_source.hpp"
|
|
||||||
|
|
||||||
namespace coop
|
|
||||||
{
|
|
||||||
|
|
||||||
// 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
|
|
||||||
@@ -148,21 +148,6 @@ void InjectionPanel::draw_hook_status(bool debug_details)
|
|||||||
|
|
||||||
const HookStatusView status = server_.hook_status();
|
const HookStatusView status = server_.hook_status();
|
||||||
|
|
||||||
// Convert the cumulative per-slot counters into rates every half second.
|
|
||||||
const double now = ImGui::GetTime();
|
|
||||||
if (now - last_sample_time_ >= 0.5)
|
|
||||||
{
|
|
||||||
const double dt = now - last_sample_time_;
|
|
||||||
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
|
||||||
{
|
|
||||||
const unsigned long long delta =
|
|
||||||
status.get_state[i] >= last_state_count_[i] ? status.get_state[i] - last_state_count_[i] : 0;
|
|
||||||
state_rate_[i] = dt > 0.0 ? static_cast<double>(delta) / dt : 0.0;
|
|
||||||
last_state_count_[i] = status.get_state[i];
|
|
||||||
}
|
|
||||||
last_sample_time_ = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SeparatorText("Hook status");
|
ImGui::SeparatorText("Hook status");
|
||||||
if (!status.attached)
|
if (!status.attached)
|
||||||
{
|
{
|
||||||
@@ -173,57 +158,13 @@ void InjectionPanel::draw_hook_status(bool debug_details)
|
|||||||
ImGui::TextColored(kGreen, "Attached (game pid %u)", status.game_pid);
|
ImGui::TextColored(kGreen, "Attached (game pid %u)", status.game_pid);
|
||||||
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");
|
||||||
|
ImGui::TextDisabled("Controller poll rates are in the Controllers panel.");
|
||||||
// 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)
|
if (!debug_details)
|
||||||
{
|
{
|
||||||
return; // everything below is diagnostic detail
|
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");
|
|
||||||
ImGui::TableSetupColumn("GetState/s");
|
|
||||||
ImGui::TableSetupColumn("GetState total");
|
|
||||||
ImGui::TableSetupColumn("GetCaps total");
|
|
||||||
ImGui::TableHeadersRow();
|
|
||||||
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
|
||||||
{
|
|
||||||
ImGui::TableNextRow();
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::Text("%d", i);
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
if (state_rate_[i] > 0.0)
|
|
||||||
{
|
|
||||||
ImGui::TextColored(kGreen, "%.0f", state_rate_[i]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui::TextDisabled("0");
|
|
||||||
}
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::Text("%llu", static_cast<unsigned long long>(status.get_state[i]));
|
|
||||||
ImGui::TableNextColumn();
|
|
||||||
ImGui::Text("%llu", static_cast<unsigned long long>(status.get_caps[i]));
|
|
||||||
}
|
|
||||||
ImGui::EndTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus-API usage: tells us whether the game even consults these (which we
|
// Focus-API usage: tells us whether the game even consults these (which we
|
||||||
// spoof) when deciding it lost focus.
|
// spoof) when deciding it lost focus.
|
||||||
ImGui::Text("Focus API calls FG:%llu Active:%llu Focus:%llu",
|
ImGui::Text("Focus API calls FG:%llu Active:%llu Focus:%llu",
|
||||||
|
|||||||
@@ -57,11 +57,6 @@ private:
|
|||||||
ImVec4 status_color_;
|
ImVec4 status_color_;
|
||||||
|
|
||||||
bool test_input_ = false;
|
bool test_input_ = false;
|
||||||
|
|
||||||
// Sampled to turn the hook's cumulative per-slot counters into poll rates.
|
|
||||||
unsigned long long last_state_count_[kMaxPads] = {};
|
|
||||||
double state_rate_[kMaxPads] = {};
|
|
||||||
double last_sample_time_ = 0.0;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace coop
|
} // namespace coop
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
|
|
||||||
#include "audio_panel.hpp"
|
#include "audio_panel.hpp"
|
||||||
#include "capture_panel.hpp"
|
#include "capture_panel.hpp"
|
||||||
|
#include "controllers_panel.hpp"
|
||||||
#include "d3d11_window.hpp"
|
#include "d3d11_window.hpp"
|
||||||
#include "debug_overlay.hpp"
|
|
||||||
#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"
|
||||||
@@ -64,6 +64,7 @@ int run()
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto input = std::make_unique<coop::XInputSource>();
|
auto input = std::make_unique<coop::XInputSource>();
|
||||||
|
coop::ControllersPanel controllers;
|
||||||
coop::InjectionPanel injection;
|
coop::InjectionPanel injection;
|
||||||
coop::AudioPanel audio;
|
coop::AudioPanel audio;
|
||||||
coop::CapturePanel capture;
|
coop::CapturePanel capture;
|
||||||
@@ -105,7 +106,7 @@ 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_controllers_panel(*input, ui.debug_details);
|
controllers.draw(*input, injection.hook_status(), ui.debug_details);
|
||||||
}
|
}
|
||||||
if (ui.show_injection)
|
if (ui.show_injection)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user