Add panel overflow instrumentation (record_panel_fit reading ImGui ScrollMax), a forced layout-reference + debug-aware center split, and host harness commands (uisize/uifit). New headless ui_fit_test drives the real Controllers + Audio panels at reference resolutions with Debug details on and asserts no panel overflows its assigned size; the Audio panel gains a demo mode so its richest content renders without a live mirror. Fixes from the measured overflow: widen the center column (was too narrow -> horizontal overflow), merge the Controllers poll/round-trip tables and fold the trigger line into the slot line, and make the center height split Debug-details-aware (Video's height is mirroring-driven, not debug-driven, so a static split can't serve both modes). Controllers + Audio now fit at 1920x1080 with max info. Video/Injection/Log fit is finalized at the end via the live uifit harness (M1 stays open until then). 15/15 ctest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
205 lines
5.8 KiB
C++
205 lines
5.8 KiB
C++
#include "controllers_panel.hpp"
|
|
|
|
#include <imgui.h>
|
|
|
|
#include "ui/app_chrome.hpp"
|
|
|
|
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());
|
|
if (debug_details)
|
|
{
|
|
// Triggers on the slot line (saves a row); thumbsticks below.
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger);
|
|
}
|
|
|
|
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("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 InputSnapshot& input, const HookStatusView& status, bool debug_details)
|
|
{
|
|
apply_panel_layout(Panel::Controllers);
|
|
ImGui::Begin("Controllers");
|
|
|
|
ImGui::Text("Input backend: %s", input.backend);
|
|
|
|
#ifdef COOP_WITH_STEAM
|
|
ImGui::Checkbox("Use Steam Input (experimental)", &steam_requested_);
|
|
if (steam_requested_)
|
|
{
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(steam_active_ ? kGreen : kGrey, steam_active_ ? "(active)" : "(starting...)");
|
|
ImGui::TextDisabled("Needs a controller bound to Steam Input for this app; otherwise");
|
|
ImGui::TextDisabled("XInput is hidden and no input arrives. Leave off for plain XInput.");
|
|
}
|
|
if (steam_note_[0] != '\0')
|
|
{
|
|
ImGui::TextColored(kGrey, "%s", steam_note_);
|
|
}
|
|
#endif
|
|
|
|
// Synthetic test input (debug aid): drives the game with a non-human pattern so
|
|
// forwarding can be proven without a real controller. Only meaningful once the
|
|
// XInput hook is attached.
|
|
if (debug_details)
|
|
{
|
|
ImGui::BeginDisabled(!status.attached);
|
|
ImGui::Checkbox("Forward synthetic test input", &test_input_);
|
|
ImGui::EndDisabled();
|
|
if (test_input_)
|
|
{
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("(ignores your controller)");
|
|
}
|
|
}
|
|
|
|
// --- 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).");
|
|
record_panel_fit("Controllers");
|
|
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: %5.0f polls/s", total_rate);
|
|
}
|
|
else
|
|
{
|
|
ImGui::TextColored(kGrey, "Game reading controller: idle");
|
|
}
|
|
|
|
// One per-slot table covers both the hook's poll counters and the input round-trip
|
|
// (what we forwarded vs what the game read back through the hook). A round-trip mismatch
|
|
// isolates a tool->game forwarding problem from an input->tool one. Merged into a single
|
|
// table so the (debug) controller view stays inside its panel even with every slot busy.
|
|
if (debug_details &&
|
|
ImGui::BeginTable("slots", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
|
|
{
|
|
ImGui::TableSetupColumn("Slot");
|
|
ImGui::TableSetupColumn("Poll/s");
|
|
ImGui::TableSetupColumn("Polls");
|
|
ImGui::TableSetupColumn("Forwarded btn/LX,LY");
|
|
ImGui::TableSetupColumn("Game read btn/LX,LY");
|
|
ImGui::TableHeadersRow();
|
|
const auto& fwd = input.pads;
|
|
for (int i = 0; i < static_cast<int>(kMaxPads); ++i)
|
|
{
|
|
const CoopPadState& f = fwd[i].state;
|
|
const CoopPadState& r = status.read_state[i];
|
|
ImGui::TableNextRow();
|
|
ImGui::TableNextColumn();
|
|
ImGui::Text("%d", i);
|
|
ImGui::TableNextColumn();
|
|
if (state_rate_[i] > 0.0)
|
|
{
|
|
ImGui::TextColored(kGreen, "%5.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("0x%04X %6d,%6d", f.buttons, f.thumb_lx, f.thumb_ly);
|
|
ImGui::TableNextColumn();
|
|
const bool match = f.buttons == r.buttons && f.thumb_lx == r.thumb_lx && f.thumb_ly == r.thumb_ly;
|
|
ImGui::TextColored(match ? kGreen : kGrey, "0x%04X %6d,%6d", r.buttons, r.thumb_lx, r.thumb_ly);
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
|
|
record_panel_fit("Controllers");
|
|
ImGui::End();
|
|
}
|
|
|
|
} // namespace coop
|