Files
SteamlessController/CMakeLists.txt
T
Dylan DeverillandClaude Opus 5 09fe805119 Tell the user when Windows is discarding the input this app injects
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>
2026-08-06 10:49:06 -07:00

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()