Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 22 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ on:
#
# Pin map (verify with `gh api repos/<owner>/<repo>/git/ref/tags/<tag>`):
# actions/checkout @ v7.0.0 → 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
# actions/setup-dotnet @ v6.0.0 → a98b56852c35b8e3190ac28c8c2271da59106c68
# actions/upload-artifact @ v7.0.1 → 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
# actions/download-artifact @ v8.0.1 → 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
# softprops/action-gh-release @ v3.0.2 → 3d0d9888cb7fd7b750713d6e236d1fcb99157228
Expand Down Expand Up @@ -192,13 +193,27 @@ jobs:
cmake --build build-msvc --config Release --target satellite
if (-not (Test-Path satellite.exe)) { throw "MSVC build did not produce satellite.exe at repo root" }

# installer.iss bundles the ViGEmBus driver from redist/, which is not
# committed -- fetch + hash-verify it first (the documented
# `pwsh scripts/fetch-redist.ps1 ; iscc installer.iss` recipe).
- name: Fetch pinned redistributables (ViGEmBus)
# installer.iss bundles the ViGEmBus driver and the HIDMaestro helper,
# neither of which is committed -- fetch + hash-verify them first (the
# documented `pwsh scripts/fetch-redist.ps1 ; dotnet publish ; iscc`
# recipe).
- name: Fetch pinned redistributables (ViGEmBus + HIDMaestro)
shell: pwsh
run: pwsh scripts/fetch-redist.ps1

- name: Set up .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: "10.0.x"

- name: Build HIDMaestro helper (dotnet publish)
shell: pwsh
run: |
dotnet publish helper/hidmaestro/satellite-hm-helper.csproj -c Release
if (-not (Test-Path "helper/hidmaestro/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/satellite-hm-helper.exe")) {
throw "dotnet publish did not produce satellite-hm-helper.exe"
}

- name: Install Inno Setup
run: choco install innosetup --no-progress

Expand Down Expand Up @@ -228,9 +243,9 @@ jobs:
dist/SatelliteSetup.exe

# The same class of assertions dish-windows runs, against the artifact
# that is about to be published rather than a rebuild of it. The driver
# is skipped (/VIGEM=skip): a kernel driver install has no business
# succeeding-or-hanging inside a CI assertion.
# that is about to be published rather than a rebuild of it. Both
# drivers are skipped (/VIGEM=skip /HIDMAESTRO=skip): a driver install
# has no business succeeding-or-hanging inside a CI assertion.
- name: Installer silent round-trip + launch smoke (no UI)
shell: pwsh
run: ./scripts/test-installer-roundtrip.ps1 -Setup dist/SatelliteSetup.exe
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/windows-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ name: Windows CI
# Pin map (verify with `gh api repos/<owner>/<repo>/git/ref/tags/<tag>`):
# actions/checkout @ v7.0.0 → 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
# actions/setup-python @ v6.2.0 → a309ff8b426b58ec0e2a45f0f869d46889d02405
# actions/setup-dotnet @ v6.0.0 → a98b56852c35b8e3190ac28c8c2271da59106c68
# actions/upload-artifact @ v7.0.1 → 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
# msys2/setup-msys2 @ v2.32.0 → 66cd2cce69caa17b53920067426061ca1de3a884

Expand Down Expand Up @@ -98,6 +99,27 @@ jobs:
test -f satellite.exe
file satellite.exe

# The HIDMaestro helper (helper/hidmaestro) is a .NET publish against
# the hash-pinned SDK release that fetch-redist.ps1 stages; building it
# here keeps PR CI honest about the installer's second driver component
# without running any driver install.
- name: Set up .NET SDK
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: "10.0.x"

- name: Fetch pinned redistributables
shell: pwsh
run: pwsh scripts/fetch-redist.ps1

- name: Build HIDMaestro helper (dotnet publish)
shell: pwsh
run: |
dotnet publish helper/hidmaestro/satellite-hm-helper.csproj -c Release
if (-not (Test-Path "helper/hidmaestro/bin/Release/net10.0-windows10.0.26100.0/win-x64/publish/satellite-hm-helper.exe")) {
throw "dotnet publish did not produce satellite-hm-helper.exe"
}

- name: Upload satellite.exe artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ compile_commands.json
# Packaged release artifacts (.deb, etc.) produced by build-deb.sh / cpack
/dist/

# Fetched redistributables (hash-pinned by redist/SHA256SUMS; .exe already
# covered) and the HIDMaestro SDK staging dir + .NET build output
/redist/*.zip
/redist/hidmaestro/
/redist/hidmaestro-staging/
/helper/hidmaestro/bin/
/helper/hidmaestro/obj/

# New-backend pure/adapter test binaries (CMake RUNTIME_OUTPUT_DIRECTORY;
# Windows .exe already covered)
/test_backend_registry
/test_gamepad_mux
/test_hidmaestro_wire
/test_hidmaestro_report

# IDE / Editor
.vs/
.vscode/
Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@
All notable connection-model and protocol changes are recorded here.
The protocol itself is specified in [`docs/contract.md`](docs/contract.md).

## Unreleased

Second Windows gamepad backend: HIDMaestro (user-mode UMDF2) alongside — not
replacing — ViGEmBus. Windows now offers all four controller types (DualSense
and Switch Pro materialize via HIDMaestro, with motion; Xbox 360 / DualShock 4
keep preferring ViGEm's kernel path and fall back to HIDMaestro when ViGEmBus
is absent). Satellite runs with either driver, both, or none.

Protocol (additive, protocol 1): `GET /api/server/capabilities` and
`GET /api/backend/status` gain a `backends` array (id, vendor, kernelMode,
availability, per-type feature + latency tiers); the singular `backend` object
is unchanged and now reports the preferred-available backend. New backend id
`hidmaestro` with error codes `DRIVER_MISSING` / `HELPER_MISSING`; new
catalog motion requires code `"hidmaestro>=1.7"`. The Windows catalog now
offers ids 2 (dualsense) and 3 (switchpro).

Installer: new optional-but-default "HIDMaestro driver" component
(`satellite-hm-helper.exe`, driver deploys at setup with no reboot), with
`/HIDMAESTRO=auto|bundled|skip` and `/REMOVEHIDMAESTRO=yes|no|auto` switches
mirroring the ViGEmBus ones.

## 1.1.0

No protocol changes. Distribution release: every shipping platform now also
Expand Down
51 changes: 51 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ include_directories(SYSTEM ${CMAKE_CURRENT_SOURCE_DIR}/lib)
set(SATELLITE_CORE_SOURCES
src/core/session_service.cpp
src/core/catalog.cpp
src/core/backend_registry.cpp
src/core/update_service.cpp
src/core/github_release.cpp
src/core/network_info.cpp
Expand Down Expand Up @@ -90,6 +91,9 @@ set(SATELLITE_WIN_SOURCES
src/platform/windows/toast.cpp
src/platform/windows/vigem.cpp
src/platform/windows/vigem_adapter.cpp
src/platform/windows/hidmaestro_wire.cpp
src/platform/windows/hidmaestro_adapter.cpp
src/platform/windows/hidmaestro_helper_client.cpp
src/platform/windows/gamepad_backend.cpp
src/platform/windows/updater_adapter.cpp
src/platform/windows/app_lifecycle.cpp
Expand Down Expand Up @@ -811,6 +815,17 @@ satellite_add_pure_test(test_config_json tests/test_config_json.cpp)
satellite_add_pure_test(test_network_info tests/test_network_info.cpp src/core/network_info.cpp)
satellite_add_pure_test(test_origin_guard tests/test_origin_guard.cpp)
satellite_add_pure_test(test_status_json tests/test_status_json.cpp)
satellite_add_pure_test(test_backend_registry tests/test_backend_registry.cpp
src/core/backend_registry.cpp)
satellite_add_pure_test(test_gamepad_mux tests/test_gamepad_mux.cpp)

# HIDMaestro shared-memory codec + per-profile report packers: pure byte /
# seqlock logic, verified on every platform even though the live backend is
# Windows-only.
satellite_add_pure_test(test_hidmaestro_wire tests/test_hidmaestro_wire.cpp
src/platform/windows/hidmaestro_wire.cpp)
satellite_add_pure_test(test_hidmaestro_report tests/test_hidmaestro_report.cpp
src/platform/windows/hidmaestro_wire.cpp)

# Portable session-crypto interop suite (net/session_crypto.cpp): the pinned
# RFC 5869 HKDF vector, the HMAC-proof vector, and the AEAD direction/counter/
Expand Down Expand Up @@ -1187,6 +1202,42 @@ if(WIN32)
add_test(NAME vigem_adapter COMMAND test_vigem_adapter)
endif()

# Drives the real hidmaestro_adapter.cpp against a fake provisioner whose
# "sections" are in-process anonymous file mappings, so the seqlock submit
# path, per-profile packing, and the output-ring rumble/lightbar workers are
# exercised without the driver, the helper, or elevation.
if(WIN32)
add_executable(test_hidmaestro_adapter
tests/test_hidmaestro_adapter.cpp
src/platform/windows/hidmaestro_adapter.cpp
src/platform/windows/hidmaestro_wire.cpp
)
target_include_directories(test_hidmaestro_adapter PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/platform/windows
)
target_include_directories(test_hidmaestro_adapter SYSTEM PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/lib
)
target_compile_definitions(test_hidmaestro_adapter PRIVATE
_WIN32_WINNT=0x0A00
)
target_link_libraries(test_hidmaestro_adapter PRIVATE
ws2_32
user32
)
if(MINGW)
target_compile_options(test_hidmaestro_adapter PRIVATE -O2 -Wall -Wextra)
target_link_options(test_hidmaestro_adapter PRIVATE -static)
elseif(MSVC)
target_compile_options(test_hidmaestro_adapter PRIVATE /O2 /W4)
endif()
set_target_properties(test_hidmaestro_adapter PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME hidmaestro_adapter COMMAND test_hidmaestro_adapter)
endif()

# Robustness fuzzing (opt-in, D-series fuzz harness): drives the receiver's
# decrypt -> inner_dispatch pipeline (net/session_crypto + net/inner_dispatch
# + core SessionService) with attacker-controlled datagram bytes, asserting
Expand Down
5 changes: 5 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ surface:
Neither set is driven by the theme tokens above; when retheming, update
the baked accent in the dashboard copies by hand.

The small UI glyphs under `web/img/icons/` (including the backend-driver
pair `driver-kernel.svg` / `driver-user.svg` used by the backend setup
guide) are original in-repo drawings in the same baked-accent flat style,
not vendored art.

## Known follow-ups (out of scope for this PR)

1. **Asset duplication**: [web/icon.png](web/icon.png) and
Expand Down
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ Runs as a system tray application with a built-in web UI for configuration. No c

**Sender** polls a physical Xbox controller via XInput at ~250 Hz and streams 12-byte `XUSB_REPORT` packets over UDP.

**Receiver** runs as a system tray app. It listens for those packets and injects them into Windows as a virtual Xbox 360 controller through the ViGEmBus kernel driver, talking to it directly via `DeviceIoControl` with no DLLs in between.
**Receiver** runs as a system tray app. It listens for those packets and injects them into Windows as a virtual controller — Xbox 360 / DualShock 4 through the ViGEmBus kernel driver (direct `DeviceIoControl`, no DLLs in between), and DualSense / Switch Pro (plus the same two as fallback) through the HIDMaestro user-mode driver (a seqlock write into the driver's shared-memory section, no managed code on the frame path).

The hot path is three syscalls with zero allocations: `recvfrom()` → `memcpy()` → `DeviceIoControl()`.
The hot path is three syscalls with zero allocations: `recvfrom()` → `memcpy()` → `DeviceIoControl()` (ViGEm) or `recvfrom()` → seqlock `memcpy()` → `SetEvent()` (HIDMaestro).

The return path carries rumble the other direction. When a game on the receiver host calls `XInputSetState` (or the equivalent on Linux's evdev FF subsystem), the platform backend fires a notification, the receiver maps it to the originating dish session, and forwards a `MSG_RUMBLE` packet back over the encrypted UDP channel. See [Rumble (return path)](#rumble-return-path) below.

Expand All @@ -42,7 +42,7 @@ The return path carries rumble the other direction. When a game on the receiver
## Prerequisites

### Receiver machine
- **Windows 10/11** with the [ViGEmBus driver](https://github.com/nefarius/ViGEmBus/releases) installed. The `SatelliteSetup.exe` installer bundles ViGEmBus 1.22.0 and installs it for you if it's missing (see [Installation](#installation)). Or:
- **Windows 10/11** with at least one virtual-gamepad driver installed — the [ViGEmBus driver](https://github.com/nefarius/ViGEmBus/releases) (Xbox 360 + DualShock 4) and/or the [HIDMaestro driver](https://github.com/hifihedgehog/HIDMaestro) (adds DualSense + Switch Pro; user-mode, no reboot). The `SatelliteSetup.exe` installer bundles both and installs them for you by default (see [Installation](#installation)); Satellite runs with either, both, or none — the offered controller types degrade accordingly. Or:
- **Linux** with the in-tree `uinput` kernel module and write access to `/dev/uinput` (see [Building → Linux](#linux) for the udev/group setup). Or:
- **macOS 10.15+** with a build carrying the `com.apple.developer.hid.virtual.device` entitlement (production builds): controllers are synthesized as virtual DualShock 4 pads via `IOHIDUserDevice`. Unentitled builds (CI artifacts, local compiles) run as development/web-UI servers only; controller descriptors apply as `backendUnavailable`.

Expand All @@ -66,22 +66,35 @@ Download `SatelliteSetup.exe` from the [Releases](https://github.com/TinkerNorth
v1.22.0 if it's missing or older. ViGEmBus 1.22.0 is the final upstream
release, so newer installations are left untouched. The Components page
shows the detected status before you continue.
- Deploy the HIDMaestro driver (optional-but-default component). It is a
user-mode UMDF2 driver embedded in the bundled `satellite-hm-helper.exe`;
deployment is idempotent and needs no reboot. It adds virtual DualSense
and Switch Pro controller types, and covers DualShock 4 / Xbox 360 when
ViGEmBus is absent.

### ViGEmBus options for unattended installs
### Driver options for unattended installs

The installer accepts a `/VIGEM=` switch alongside Inno Setup's standard
`/SILENT` / `/VERYSILENT`:
The installer accepts `/VIGEM=` and `/HIDMAESTRO=` switches alongside Inno
Setup's standard `/SILENT` / `/VERYSILENT`:

| Switch | Behavior |
|---|---|
| *(none)* / `/VIGEM=auto` | Default. Install the bundled ViGEmBus only if missing or older than 1.22.0. |
| `/VIGEM=bundled` | Force-run the bundled installer regardless of what's already there. |
| `/VIGEM=skip` | Don't touch the driver. Use this on locked-down machines or when ViGEmBus is managed externally. |
| *(none)* / `/HIDMAESTRO=auto` | Default. Deploy/refresh the bundled HIDMaestro driver (idempotent, no reboot). |
| `/HIDMAESTRO=skip` | Don't touch the HIDMaestro driver. |

A reboot is sometimes required on first ViGEmBus install (MSI exit code 3010).
The Satellite installer surfaces this as a "Restart now / later" prompt
on the final wizard page. Until you reboot, virtual-gamepad output may not
work even though the driver is installed.
work even though the driver is installed. HIDMaestro never needs a reboot.

At runtime, the first HIDMaestro controller you plug in each Satellite
session shows one Windows elevation prompt: creating the virtual device
needs administrator rights, which Satellite (running unelevated) delegates
to `satellite-hm-helper.exe` for that session. ViGEm-only sessions never
see a prompt.

### Uninstalling

Expand All @@ -99,19 +112,24 @@ the uninstaller from the Start Menu. The uninstaller will:
- Ask whether to also uninstall the ViGEmBus driver. The default is
No, because other apps (DS4Windows, BetterJoy, MoonDeck-Buddy, etc.)
commonly share it and removing it would break them silently.
- Ask the same, separately, for the HIDMaestro driver (other HIDMaestro
consumers such as PadForge may share it; default No).

User configuration (`%APPDATA%\satellite\config.json` and the pairing
keyfile) is left in place, so a reinstall preserves your paired
clients. Delete that folder by hand if you want a clean wipe.

The uninstaller accepts a `/REMOVEVIGEM=` switch alongside Inno Setup's
standard `/SILENT`:
The uninstaller accepts `/REMOVEVIGEM=` and `/REMOVEHIDMAESTRO=` switches
alongside Inno Setup's standard `/SILENT`:

| Switch | Behavior |
|---|---|
| *(none)* / `/REMOVEVIGEM=auto` | Default. Prompt the user; on `/SILENT` uninstall, do not touch the driver. |
| `/REMOVEVIGEM=yes` | Always uninstall the ViGEmBus driver as part of removing Satellite. |
| `/REMOVEVIGEM=no` | Never uninstall the ViGEmBus driver. Suppresses the prompt. |
| *(none)* / `/REMOVEHIDMAESTRO=auto` | Default. Prompt the user; on `/SILENT` uninstall, do not touch the driver. |
| `/REMOVEHIDMAESTRO=yes` | Always remove the HIDMaestro driver as part of removing Satellite. |
| `/REMOVEHIDMAESTRO=no` | Never remove the HIDMaestro driver. Suppresses the prompt. |

## Building

Expand Down Expand Up @@ -556,8 +574,10 @@ On macOS, the same DS4 output report `0x05` that carries the motors also
carries the lightbar colour; the IOHIDUserDevice set-report path fans it out
to the lightbar callback when the report's colour-valid flag is set (entitled
builds only). The Linux uinput backend has no host-driven lightbar channel
(EV_LED is single-bit), so it never emits `MSG_LIGHTBAR`. Lightbar emission
is a Windows / ViGEm-DS4 and macOS / IOHID-DS4 feature today.
(EV_LED is single-bit), so it never emits `MSG_LIGHTBAR`, and the backend
registry advertises `lightbar: false` for its Sony types so the catalog never
offers a colour that cannot arrive. Lightbar emission is a Windows
(ViGEm-DS4 and HIDMaestro DS4/DualSense) and macOS / IOHID-DS4 feature today.

### Per-dish actuator behaviour

Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Out of scope:
on the user's PC (root, Administrator, ability to drop binaries in
`%APPDATA%`, etc.).
- The vendored ViGEmBus driver itself; file with [nefarius/ViGEmBus](https://github.com/nefarius/ViGEmBus).
- The bundled HIDMaestro driver/SDK itself; file with [hifihedgehog/HIDMaestro](https://github.com/hifihedgehog/HIDMaestro).
- DoS via raw network flooding. UDP without rate-limit is a known
trade-off for hot-path latency; mitigations belong in the network
fabric, not the protocol.
Expand Down
Loading
Loading