M1: UI-fit instrumentation + headless ui_fit_test + first overflow fixes

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>
This commit is contained in:
2026-06-22 11:14:31 +02:00
parent fc43355db2
commit cf7e0c783a
11 changed files with 409 additions and 62 deletions

View File

@@ -37,6 +37,12 @@ void draw_pad(int index, const PadInfo& pad, bool debug_details)
}
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: ");
@@ -57,7 +63,6 @@ void draw_pad(int index, const PadInfo& pad, bool debug_details)
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);
}
@@ -117,6 +122,7 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
if (!status.attached)
{
ImGui::TextDisabled("Not injected (no XInput hook).");
record_panel_fit("Controllers");
ImGui::End();
return;
}
@@ -150,16 +156,24 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
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", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
ImGui::BeginTable("slots", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Slot");
ImGui::TableSetupColumn("GetState/s");
ImGui::TableSetupColumn("GetState total");
ImGui::TableSetupColumn("GetCaps total");
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);
@@ -175,42 +189,15 @@ void ControllersPanel::draw(const InputSnapshot& input, const HookStatusView& st
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::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();
}
// Round-trip view: what we forwarded (the active backend's pad, tagged per slot in
// the Incoming section above) vs what the game actually read back through the hook.
// A mismatch isolates a tool->game forwarding problem from an input->tool one.
if (debug_details)
{
ImGui::SeparatorText("Round-trip (forwarded vs game read)");
if (ImGui::BeginTable("roundtrip", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Slot");
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();
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();
}
ImGui::TextDisabled("Slot tags in 'Incoming' above show which backend (XInput / Steam) fed each slot.");
}
record_panel_fit("Controllers");
ImGui::End();
}