The core fix: the SC2026 firmware silently reverts to lizard mode (including
its autonomous click haptics) after a period without host feature reports,
which doubled up with software haptics as a crunchy burst. A keepalive now
re-asserts the lizard-off state every 2 seconds while game mode is active.
Haptics reliability and feel:
- Switch trackpad haptics from OUT_HAPTIC_PULSE (grip LRAs) to
OUT_HAPTIC_COMMAND (trackpad LRAs) for localized feel
- Click haptic latch: press fires off the debounced firmware click bit,
release only fires once contact area returns to idle, so force chatter
and mid-hold easing cannot re-fire
- Movement ticks: distance-based with motion deadzone, post-release and
first-touch grace windows, pressing/teleport gates (pause, not reset),
idle decay, and a 50ms send rate limit so firmware waveforms never queue
into a backlog
Handoff and mouse:
- EnableLizardMode now also sends LOAD_DEFAULT_SETTINGS so firmware mouse
emulation resumes immediately on disable instead of waiting for the
firmware revert timeout
- Trackpad mouse: carry sub-pixel movement remainders between frames and
retune sensitivity (0.015 -> 0.01125)
Also: shared/exclusive HID reopen support, local-only HapticTester tool
(gitignored, conditional CMake target), installer version bump to 1.7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the per-frame movement buzz with a scroll-wheel-style tick that
fires once per 5000 raw units of travel. Adds a matching haptic on click
release to mirror Steam's press/release feel. Reseeds the movement
baseline on click release so stale position delta doesn't bleed into a
spurious movement tick on the same frame.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PlayStation mode now emits DS4_REPORT_EX on every frame:
- Raw gyro and accelerometer forwarded from SC IMU (enabled via
SETTING_IMU_MODE when PS4 mode is active; disabled on teardown)
- SC right/left trackpads mapped to DS4 touchpad contacts 1/2,
with correct 12-bit XY packing and per-gesture contact IDs;
pads claimed for mouse use are suppressed from the touchpad report
- Battery level and charge state forwarded from REPORT_BATTERY_STATUS
(0x43) reports received in the read loop
- DS4 output moved from deprecated notification callback to a polling
thread using vigem_target_ds4_await_output_report_timeout()
Enhanced haptics applied to both Xbox and PlayStation modes:
- Power curve (value^0.68) and cross-channel mixing (large motor
bleeds into both haptic actuators; small motor weighted toward right)
- Short attack boost on rising motor edges to replicate ERM punch
- Trackpad touch and click fire one-shot haptic pulses via new
OUT_HAPTIC_PULSE (0x81) and OUT_HAPTIC_COMMAND (0x82) output reports
Additional fixes:
- REPORT_SECONDARY = 0x43 renamed to REPORT_BATTERY_STATUS
- ViGEMClient upgraded from 1.16.18.0 to b66d02d snapshot which
exposes DS4_REPORT_EX, vigem_target_ds4_update_ex(), and
vigem_target_ds4_await_output_report_timeout()
Co-authored-by: david419kr <david419kr@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a ControllerPlatform enum (Xbox, PlayStation) and a full
DualShock 4 translation path alongside the existing Xbox 360 one.
Switching platforms live tears down and recreates the virtual device.
The selection persists in the registry and is exposed via a
"Controller Platform" submenu in the tray context menu.
DS4 translation maps face buttons to Cross/Circle/Square/Triangle,
encodes the D-pad as a HAT switch (with diagonal support), converts
signed 16-bit axes to uint8 with Y-axis inversion, and routes right
trackpad clicks to the DS4 touchpad button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/utf-8 enforces UTF-8 source encoding, which makes C4828 fire on
ViGEmClient.h (non-UTF-8 bytes in its comments). Only SteamlessController
includes ViGEm headers (via VirtualController.cpp), so /wd4828 is needed
there specifically. Other targets remain clean.
Co-Authored-By: Dylan Deverill <ddeverill@gmail.com>
Conflicts resolved:
- ControllerManager.cpp: take main's slot-based EnableGameMode/
DisableGameMode dispatch; wire the rumble callback into
EnableGameModeSlot and add SetRumble(0,0) to DisableGameModeSlot
so rumble stops cleanly before vc is torn down.
- VirtualController.h: keep both RumbleCallback/OnRumble (ours) and
the new Update(backCfg, backMouseEnabled) signature (main's).
Co-Authored-By: Dylan Deverill <ddeverill@gmail.com>
Same latent use-after-free as WriteOutputReport: after CancelIo, the
kernel can still complete the IRP and write into the stack OVERLAPPED
after the function returns. Block with GetOverlappedResult(..., TRUE)
to drain before ov goes out of scope.
Co-Authored-By: Dylan Deverill <ddeverill@gmail.com>
- Drop Tick()/m_wantsGameMode auto-enable machinery (out of scope for
this PR — belongs in a separate submission)
- Remove unused CMD_TRIGGER_RUMBLE constant
- Fix WriteOutputReport comment to accurately describe padding behaviour
- Fix CancelIo OVERLAPPED use-after-free: drain with GetOverlappedResult
before the stack OVERLAPPED goes out of scope
- Strip redundant /wd4819 /wd4828 MSVC flags; /utf-8 already covers them
- Add comment to HeartbeatLoop explaining the 40 ms tick rationale
Co-Authored-By: Dylan Deverill <ddeverill@gmail.com>
Introduces a WebView2-based remap window (double-click tray icon or
'Remap Back Buttons...' context menu) that lets users assign any of
L4/L5/R4/R5 to a face button, bumper, trigger, d-pad direction, stick
click, Menu/View, mouse button, or 'none' (natural behavior).
- BackButtonConfig: new enum + struct; values stable as registry DWORDs
- RemapWindow: chromeless WebView2 window with a button picker and
press-to-detect capture mode; settings persist across launches
- ControllerManager: StartButtonCapture/StopButtonCapture API with
rising-edge HID detection for all mappable targets
- VirtualController: ApplyBackAction injects the mapped XInput button
each frame; mouse-button actions are edge-detected in the read loop
- TrackpadMouse: back-button mouse-click handling decoupled from remap
- TrayApp: wires up the remap window, saves/loads all back button
settings to HKCU\Software\SteamlessController
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each connected controller now gets its own virtual Xbox 360 device via
ViGEmBus, mapping to separate XInput player slots (1, 2, 3...).
Key changes:
- SteamController: add EnumerateAll() to probe and return all live
device paths across wired (0x1302) and dongle (0x1304) PIDs; add
Open(path) overload to open a specific already-validated path.
- ControllerManager: replace single-controller state with a vector of
Slot structs, each owning its own SteamController, VirtualController,
TrackpadMouse, and read thread. SyncDevices() diffs live paths against
current slots on every device-change event, adding new controllers and
removing disconnected ones. Controllers hot-plugged while game mode is
active are automatically brought into game mode.
- Bump installer version to 1.3.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A Valve firmware update changed the main STATE report ID from 0x42 to
0x45. REPORT_STATE is updated to match; the now-redundant REPORT_EXTENDED
constant (which previously held 0x45) is removed.
Also adds better probe diagnostics to Open() — distinguishes between an
unexpected report ID (firmware mismatch) and a read timeout (empty slot).
Fixes a stale debug flag in SteamProbe: FORCE_SHOW_BYTES_16_17 reverted
to false (right joystick Y, not X).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Steam Controller Puck exposes up to four FF00/0001 HID interfaces
(one per paired controller slot). Previously, Open() blindly picked the
first interface, which could be an empty slot — feature reports sent to
it were silently ignored, so lizard mode was never disabled.
Now Open() iterates all FF00/0001 interfaces for both the wired PID
(0x1302) and the dongle PID (0x1304), reads a single input report with
a 500 ms timeout, and latches onto the first interface that responds
with a live 0x42 STATE report.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added EnumerateAllValveDevices() to SteamProbe which lists every HID
interface with VID=0x28DE at startup — printing PID, usage page, usage,
and product string. This makes it easy to identify the wireless dongle's
PID without needing Device Manager.
Updated HidDevice::Enumerate to treat pid=0 as a wildcard so callers
can enumerate all devices for a given vendor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Start with Windows toggle in tray menu — writes/removes the Run registry
key so the app launches automatically at login
- Inno Setup installer script for packaging the release exe
- CMakePresets.json for named debug/release build configurations
- README updated with correct Visual Studio 2025 build commands and a
note for VS 2022 users
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>