mirror of
https://github.com/ddeverill/SteamlessController.git
synced 2026-10-22 06:32:20 -05:00
Users reported the trackpad going dead after a takeover — haptics still
firing, cursor not moving — and recovering the moment focus moved to
another window or the window in front was closed. Haptics are HID writes
and the virtual pad is ViGEm, so both keep working regardless; SendInput
was the one stage of the pipeline that could fail and leave no trace.
Reproduced on this machine with an elevated Device Manager in front:
- 137 px of movement injected, cursor pinned at (910,781) for 5s
- foreground mmc.exe at High integrity, this process at Medium
- SendInput returned success for every one of those events
That last point decides the shape of the fix. UIPI is documented as
failing the call with ERROR_ACCESS_DENIED, but on Windows 11 26200 the
call reports success and the input is dropped afterwards, so no
return-value check can see it. The condition is only observable by asking,
before injecting, whether the foreground window outranks this process.
So that is what Send now does, on every event:
- one check covers cursor movement, trackpad clicks, paddle buttons and
keys, none of which can be judged from their own result. A blocked
paddle press previously produced no evidence anywhere
- the foreground lookup is cached against the window it describes;
injection happens at report rate, foreground changes do not
- the state changing is what gets logged, not the events: one line
naming the blocker, one when it stops
- the tray balloon says which application is blocking input and that
switching away from it restores the controller, rate-limited to one
per blocker per minute so alt-tabbing cannot spam it
- the check never gates the event. It observes and reports; if it is
ever wrong about a system, the input goes out exactly as before
A foreground process whose token cannot be read is deliberately not
named. From Low integrity, OpenProcess succeeds while OpenProcessToken
fails with ERROR_ACCESS_DENIED, and protected processes refuse it too —
so an unreadable level means unknown, not blocked. Guessing would put a
wrong application's name in front of the user; the snapshot records
"unreadable" instead.
Backing this up, for the causes that produce no error and no integrity
mismatch: injected movement is checked against the cursor actually
moving, which is what catches a cursor clip a game left behind or a hook
filtering injected events. It stays quiet whenever a blocker is already
known, so it cannot contradict the better explanation. A refused call is
still logged with a dropped-event count and a recovery line, as a cheap
catch-all for systems where the call does fail.
The read loop also logs the shape of the first state report it gets.
TrackpadMouse requires 30 bytes where the haptics need 28, so a short
report would produce working haptics and a dead cursor — the same symptom
from a completely different cause, and not one that can be told apart
from a user's description. This machine reports 0x42 at 54 bytes over
USB, which rules it out here but not on transports we have less evidence
for.
EventLog::Write is annotated _Printf_format_string_ so a mismatched
specifier is caught by /analyze. It is not enforced in the normal build,
but these lines are read long after the fact by someone trying to explain
a fault they cannot reproduce, and the paths that log least often are
exactly the ones no test run exercises.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
119 lines
3.6 KiB
CMake
119 lines
3.6 KiB
CMake
cmake_minimum_required(VERSION 3.20)
|
|
project(SteamlessController CXX)
|
|
|
|
include(FetchContent)
|
|
|
|
set(CMAKE_CXX_STANDARD 20)
|
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
|
|
# Shared HID + Steam Controller library code
|
|
add_library(SteamController STATIC
|
|
src/hid/HidDevice.cpp
|
|
src/steam/SteamController.cpp
|
|
)
|
|
target_include_directories(SteamController PUBLIC src)
|
|
target_link_libraries(SteamController PUBLIC hid setupapi)
|
|
if (MSVC)
|
|
target_compile_options(SteamController PRIVATE /W4 /WX /utf-8)
|
|
endif()
|
|
|
|
# Diagnostic probe tool (console)
|
|
add_executable(SteamProbe
|
|
src/main.cpp
|
|
)
|
|
target_link_libraries(SteamProbe PRIVATE SteamController)
|
|
if (MSVC)
|
|
target_compile_options(SteamProbe PRIVATE /W4 /WX /utf-8)
|
|
set_target_properties(SteamProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
|
|
endif()
|
|
|
|
# ViGEmClient (built from source, static)
|
|
add_library(ViGEmClient STATIC
|
|
third_party/ViGEmClient-b66d02d/src/ViGEmClient.cpp
|
|
)
|
|
target_include_directories(ViGEmClient PUBLIC
|
|
third_party/ViGEmClient-b66d02d/include
|
|
)
|
|
target_link_libraries(ViGEmClient PUBLIC setupapi)
|
|
if (MSVC)
|
|
target_compile_options(ViGEmClient PRIVATE /W0 /utf-8 /wd4828)
|
|
endif()
|
|
|
|
# RawGameController diagnostic probe (WinRT)
|
|
add_executable(RawControllerProbe
|
|
src/probe/RawControllerProbe.cpp
|
|
)
|
|
target_link_libraries(RawControllerProbe PRIVATE windowsapp)
|
|
if (MSVC)
|
|
target_compile_options(RawControllerProbe PRIVATE /W4 /utf-8)
|
|
endif()
|
|
|
|
# Elevated on-demand helper — restarts the controller device node so the tray
|
|
# app (which never runs elevated) can take the device from a running Steam.
|
|
add_executable(SteamlessDeviceCycle WIN32
|
|
src/helper/DeviceCycleHelper.cpp
|
|
src/app/DeviceRestart.cpp
|
|
src/app/HandleFinder.cpp
|
|
)
|
|
target_link_libraries(SteamlessDeviceCycle PRIVATE SteamController cfgmgr32)
|
|
if (MSVC)
|
|
target_compile_options(SteamlessDeviceCycle PRIVATE /W4 /WX /utf-8)
|
|
endif()
|
|
|
|
# HapticTester — local-only interactive haptic tuning tool (source is gitignored)
|
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/probe/HapticTester.cpp")
|
|
add_executable(HapticTester src/probe/HapticTester.cpp)
|
|
target_link_libraries(HapticTester PRIVATE SteamController)
|
|
if (MSVC)
|
|
target_compile_options(HapticTester PRIVATE /W4 /utf-8)
|
|
set_target_properties(HapticTester PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
|
|
endif()
|
|
endif()
|
|
|
|
# WebView2 — downloaded from NuGet (.nupkg is a zip; FetchContent extracts it)
|
|
FetchContent_Declare(WebView2
|
|
URL https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2849.39
|
|
DOWNLOAD_EXTRACT_TIMESTAMP ON
|
|
)
|
|
FetchContent_MakeAvailable(WebView2)
|
|
|
|
# Map CMake architecture to the NuGet lib subdirectory name
|
|
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
|
set(WV2_ARCH "x64")
|
|
else()
|
|
set(WV2_ARCH "x86")
|
|
endif()
|
|
|
|
# System tray app (no console window)
|
|
add_executable(SteamlessController WIN32
|
|
src/app/main.cpp
|
|
src/app/TrayApp.cpp
|
|
src/app/ControllerManager.cpp
|
|
src/app/VirtualController.cpp
|
|
src/app/TrackpadMouse.cpp
|
|
src/app/KeyInput.cpp
|
|
src/app/InputInjection.cpp
|
|
src/app/SteamWatcher.cpp
|
|
src/app/DeviceRestart.cpp
|
|
src/app/EventLog.cpp
|
|
src/app/HandleFinder.cpp
|
|
src/app/RemapWindow.cpp
|
|
resources/app.rc
|
|
)
|
|
target_include_directories(SteamlessController PRIVATE
|
|
resources
|
|
${webview2_SOURCE_DIR}/build/native/include
|
|
)
|
|
target_link_libraries(SteamlessController PRIVATE
|
|
SteamController
|
|
ViGEmClient
|
|
${webview2_SOURCE_DIR}/build/native/${WV2_ARCH}/WebView2LoaderStatic.lib
|
|
version
|
|
ole32
|
|
shlwapi
|
|
cfgmgr32
|
|
)
|
|
if (MSVC)
|
|
target_compile_options(SteamlessController PRIVATE /W4 /WX /utf-8 /wd4828)
|
|
endif()
|