Files
Dylan DeverillandClaude Sonnet 5.5 1409bb131e Add a blanket stick deadzone (#117)
A hand-off stick does not read exactly zero, and the virtual pad passed
whatever it read straight through. XInput games mostly hide that behind a
deadzone of their own; an emulator waiting for "press the input to bind", or
a gamepad tester, takes the pad's word for it and sees the sticks moving all
the time, so a bind prompt latches onto them before a button can be pressed.

StickRestProbe measured what a deadzone has to clear, hands off, on two
units. The reading is a steady offset from zero rather than noise: jitter
about the mean is under 0.5%, while the offset reaches 5.9% of the range on
one stick and wanders by over a point from one settle to the next. So the
deadzone is 8%, about 1.4 times the worst resting reading, rather than a
number that clears the measurement by a hair.

Radial rather than per-axis, so a direction is left alone, and rescaled so
the output does not jump the moment the stick leaves the deadzone. The scale
is applied per axis and each axis clamped on its own, so a diagonal pushed
into a stick's corner still reaches full on both axes as it did before.

Applied in one place, ReadStick, which both the Xbox and DualShock 4 reports
read their sticks through, so the two cannot disagree about what rest is.
Fixed rather than a setting: a stick does not need tuning per game the way a
trigger threshold does.

Also invert the DualShock 4 Y axis before quantising it rather than after.
255 - u8 turned a centred 128 into 127, so a stick at rest sat one step off
on Y and nowhere near it on X, which a gamepad tester showed as a constant
nonzero value.

StickDeadzoneProbe checks the maths without a controller, including that the
worst measured resting position reads as nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 18:44:20 -07:00

276 lines
9.7 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()
# Game-library diagnostic probe — dumps every source the picker draws on
add_executable(GameLibraryProbe
src/probe/GameLibraryProbe.cpp
src/app/GameLibrary.cpp
src/app/LauncherGames.cpp
src/app/ProcessIdentity.cpp
)
target_include_directories(GameLibraryProbe PRIVATE src)
target_link_libraries(GameLibraryProbe PRIVATE ole32 shell32 shlwapi version windowsapp)
if (MSVC)
target_compile_options(GameLibraryProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(GameLibraryProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# Foreground-watcher diagnostic probe — prints what the per-game profile
# matcher would see as the user switches applications
add_executable(ForegroundProbe
src/probe/ForegroundProbe.cpp
src/app/ForegroundWatcher.cpp
src/app/ProcessIdentity.cpp
)
target_include_directories(ForegroundProbe PRIVATE src)
if (MSVC)
target_compile_options(ForegroundProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(ForegroundProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# Steam manifest diagnostic probe — dumps the app id to install directory map
# that per-game profiles for Steam games are matched through
add_executable(SteamAppProbe
src/probe/SteamAppProbe.cpp
src/app/SteamAppLocator.cpp
)
target_include_directories(SteamAppProbe PRIVATE src)
if (MSVC)
target_compile_options(SteamAppProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(SteamAppProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# Binding encoding checks — the packed and wire forms both have to keep
# decoding values written by earlier builds
add_executable(BindingCodecProbe
src/probe/BindingCodecProbe.cpp
)
target_include_directories(BindingCodecProbe PRIVATE src)
if (MSVC)
target_compile_options(BindingCodecProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(BindingCodecProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# Dual-stage trigger checks — the detector's hysteresis and mode changes, the
# clamps on persisted thresholds, and that profiles written before the feature
# read back as a plain analog trigger. No controller needed.
add_executable(TriggerDetectorProbe
src/probe/TriggerDetectorProbe.cpp
)
target_include_directories(TriggerDetectorProbe PRIVATE src)
if (MSVC)
target_compile_options(TriggerDetectorProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(TriggerDetectorProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# Virtual gamepad bus probe — dumps every bus the ViGEm client can see, which
# is the one question a "vigem_connect failed" report cannot answer on its own
add_executable(ViGEmBusProbe
src/probe/ViGEmBusProbe.cpp
src/app/ViGEmBusInfo.cpp
)
target_include_directories(ViGEmBusProbe PRIVATE src)
target_link_libraries(ViGEmBusProbe PRIVATE ViGEmClient)
if (MSVC)
target_compile_options(ViGEmBusProbe PRIVATE /W4 /WX /utf-8 /wd4828)
set_target_properties(ViGEmBusProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
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()
# Directional pad checks — the sector maths wraps at 0/360, holds a sector
# against hysteresis, and latches a press's zone, none of which fail loudly
add_executable(TrackpadDirectionProbe
src/probe/TrackpadDirectionProbe.cpp
src/app/TrackpadInput.cpp
src/app/InputInjection.cpp
src/app/EventLog.cpp
)
target_include_directories(TrackpadDirectionProbe PRIVATE src)
target_link_libraries(TrackpadDirectionProbe PRIVATE SteamController)
if (MSVC)
target_compile_options(TrackpadDirectionProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(TrackpadDirectionProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# TrackpadZoneProbe — measures where a thumb lands when it clicks a pad, so the
# Directional Pad ring boundary is calibrated rather than guessed.
add_executable(TrackpadZoneProbe
src/probe/TrackpadZoneProbe.cpp
)
target_link_libraries(TrackpadZoneProbe PRIVATE SteamController)
if (MSVC)
target_compile_options(TrackpadZoneProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(TrackpadZoneProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# TriggerProbe — measures trigger rest/travel/speed/jitter and benches haptic
# click candidates, so a simulated dual-stage trigger (issue #75) has real
# numbers behind its threshold and its feedback.
add_executable(TriggerProbe
src/probe/TriggerProbe.cpp
)
target_link_libraries(TriggerProbe PRIVATE SteamController)
if (MSVC)
target_compile_options(TriggerProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(TriggerProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# Stick deadzone checks — rest reads as nothing, the edge has no step, and full
# deflection still reaches full. No controller needed.
add_executable(StickDeadzoneProbe
src/probe/StickDeadzoneProbe.cpp
)
target_include_directories(StickDeadzoneProbe PRIVATE src)
if (MSVC)
target_compile_options(StickDeadzoneProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(StickDeadzoneProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# StickRestProbe — measures what the sticks report at rest, so the stick
# deadzone (issue #117) is sized from numbers rather than a guess.
add_executable(StickRestProbe
src/probe/StickRestProbe.cpp
)
target_link_libraries(StickRestProbe PRIVATE SteamController)
if (MSVC)
target_compile_options(StickRestProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(StickRestProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
endif()
# PowerOffProbe — tries the firmware "turn off controller" command candidates and
# watches whether the pad goes quiet, so Steam + Y can power it down (issue #106).
add_executable(PowerOffProbe
src/probe/PowerOffProbe.cpp
)
target_link_libraries(PowerOffProbe PRIVATE SteamController)
if (MSVC)
target_compile_options(PowerOffProbe PRIVATE /W4 /WX /utf-8)
set_target_properties(PowerOffProbe PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
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/TrackpadInput.cpp
src/app/KeyInput.cpp
src/app/TouchKeyboard.cpp
src/app/InputInjection.cpp
src/app/SteamWatcher.cpp
src/app/DeviceRestart.cpp
src/app/EventLog.cpp
src/app/ForegroundWatcher.cpp
src/app/GameLibrary.cpp
src/app/GameLiveness.cpp
src/app/GameProfiles.cpp
src/app/HandleFinder.cpp
src/app/LauncherGames.cpp
src/app/ProcessIdentity.cpp
src/app/RemapWindow.cpp
src/app/SteamAppLocator.cpp
src/app/ViGEmBusInfo.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
shell32
shlwapi
cfgmgr32
windowsapp
)
if (MSVC)
target_compile_options(SteamlessController PRIVATE /W4 /WX /utf-8 /wd4828)
endif()