Phase 1a: per-slot poll viz + input-path diagnostics
Surfaced by a game (Life is Strange: Before the Storm) that ignores controller input when it lacks true OS focus even though it still polls XInput. To find the focus-gated detection path, instrument the hook. Protocol v3 status back-channel now reports: - per-slot XInputGetState and XInputGetCapabilities counters (replacing the single aggregate), so the overlay shows exactly which slots the game polls and how fast; - focus-API call counts (GetForegroundWindow/GetActiveWindow/GetFocus) to see whether the game consults the APIs we spoof; - input-path diagnostics: whether the process registered Raw Input for a gamepad usage and whether it set RIDEV_INPUTSINK (background delivery), and whether a DirectInput dll is loaded. Host overlay gains a per-slot poll table and an "Input path" section. The DLL refreshes input diagnostics each worker tick via GetRegisteredRawInputDevices. hook_selftest updated for per-slot counters; passes. This is diagnostic-only: once a real run shows which path LiS uses, the targeted focus fix (e.g. forcing RIDEV_INPUTSINK or DI background coop) follows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -148,15 +148,18 @@ void InjectionPanel::draw_hook_status()
|
||||
|
||||
const HookStatusView status = server_.hook_status();
|
||||
|
||||
// Convert the cumulative query counter into a rate every half second.
|
||||
// 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_;
|
||||
const unsigned long long delta =
|
||||
status.xinput_queries >= last_query_count_ ? status.xinput_queries - last_query_count_ : 0;
|
||||
query_rate_ = dt > 0.0 ? static_cast<double>(delta) / dt : 0.0;
|
||||
last_query_count_ = status.xinput_queries;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -169,14 +172,65 @@ void InjectionPanel::draw_hook_status()
|
||||
|
||||
ImGui::TextColored(kGreen, "Attached (game pid %u, hwnd 0x%llX)", status.game_pid,
|
||||
static_cast<unsigned long long>(status.game_hwnd));
|
||||
ImGui::Text("XInput polled: %.0f/s (last slot %u)", query_rate_, status.last_user_index);
|
||||
if (query_rate_ > 0.0)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(kGreen, " <- game is reading our input");
|
||||
}
|
||||
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.
|
||||
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
|
||||
// spoof) when deciding it lost focus.
|
||||
ImGui::Text("Focus API calls FG:%llu Active:%llu Focus:%llu",
|
||||
static_cast<unsigned long long>(status.focus_calls[coop::FocusApi_Foreground]),
|
||||
static_cast<unsigned long long>(status.focus_calls[coop::FocusApi_Active]),
|
||||
static_cast<unsigned long long>(status.focus_calls[coop::FocusApi_Focus]));
|
||||
|
||||
// Input-path diagnostics: a focus-gated detection path would explain a game
|
||||
// that only accepts the controller when it has true focus.
|
||||
ImGui::SeparatorText("Input path");
|
||||
if (status.raw_input_gamepad)
|
||||
{
|
||||
ImGui::TextColored(status.raw_input_gamepad_sink ? kGreen : kRed, "Raw Input gamepad: yes (INPUTSINK %s)",
|
||||
status.raw_input_gamepad_sink ? "set -> bg ok" : "MISSING -> focus-gated!");
|
||||
}
|
||||
else if (status.raw_input_registered)
|
||||
{
|
||||
ImGui::TextColored(kGrey, "Raw Input: registered, but not for a gamepad usage");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextColored(kGrey, "Raw Input: not registered");
|
||||
}
|
||||
ImGui::TextColored(status.dinput_loaded ? kRed : kGrey, "DirectInput dll loaded: %s",
|
||||
status.dinput_loaded ? "yes (could be foreground-gated)" : "no");
|
||||
}
|
||||
|
||||
void InjectionPanel::draw()
|
||||
|
||||
Reference in New Issue
Block a user