Skip to content

feat(backend): add HIDMaestro as a second Windows gamepad backend alongside ViGEmBus - #82

Open
emir-hasanbegovic wants to merge 3 commits into
mainfrom
feat/hidmaestro-backend
Open

feat(backend): add HIDMaestro as a second Windows gamepad backend alongside ViGEmBus#82
emir-hasanbegovic wants to merge 3 commits into
mainfrom
feat/hidmaestro-backend

Conversation

@emir-hasanbegovic

@emir-hasanbegovic emir-hasanbegovic commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Adds HIDMaestro (user-mode UMDF2, MIT) as a second Windows gamepad backend alongside — not replacing — ViGEmBus. Windows now offers all four controller types; the app, installer, and web UI work with 0, 1, or 2 drivers installed.

What each backend does

Type Both installed ViGEm only HIDMaestro only
Xbox 360 ViGEm (kernel, lowest latency) ViGEm HIDMaestro
DualShock 4 (motion/touchpad/lightbar) ViGEm ViGEm HIDMaestro
DualSense (motion/touchpad/lightbar) HIDMaestro HIDMaestro
Switch Pro (motion) HIDMaestro HIDMaestro

Design

  • Registry + mux (pure core). core/backend_registry models backends as data (id, vendor, kernel/user mode, per-type features + latency tiers); core/gamepad_mux is a composite IGamepadPort routing one shared serial space across preference-ordered children. SessionService is untouched — same single port reference, same serial pool.
  • Hot path stays native and allocation-free. The HIDMaestro submit is pack (fixed per-slot buffers) → seqlock memcpy into the driver's mapped shared-memory section → SetEvent. No managed code, no syscall beyond the doorbell, no allocations per frame. Rumble/lightbar come back through a native reader of the driver's 64-slot output ring (one worker thread per plugged pad, driver-signaled doorbell), mirroring the ViGEm notification workers.
  • Elevation is brokered, satellite stays asInvoker. Device lifecycle (SwDevice + Global\ section creation, driver deploy) needs an elevated token, so it lives in the bundled self-contained .NET helper (helper/hidmaestrosatellite-hm-helper.exe). Satellite spawns it once per session on the first HIDMaestro plug (one UAC prompt; never from a status probe — ViGEm-only sessions never see it), and the helper duplicates the per-controller section/event handles back over a private named pipe (client PID verified against the spawned process). If the helper dies, submits keep working; unplug reports unconfirmed and quarantines the serial, matching the ViGEm zombie-target contract.
  • Wire contract is pinned, not guessed. HIDMaestro's shared-memory protocol has no version field, so hidmaestro_wire.h + hidmaestro_report.h re-state the v1.7.0 layout (section sizes are the runtime layout check) and lib/VENDORED.md documents the pin-bump procedure. The DS4 packer reuses the already-validated codec hoisted to core/ds4_report.h (macOS + Windows now share it byte-for-byte); Sony IMU values are rescaled to the driver's neutral-calibration units (20 LSB/deg·s, 10000 LSB/g); the Switch body follows SDL's Nintendo mapping exactly like the Linux uinput adapter.

API / UI (additive, protocol 1)

  • GET /api/server/capabilities and GET /api/backend/status gain a backends array (identity, availability, per-type features, comparable latency ranks). The singular backend object is unchanged in shape and now reports the preferred-available backend, so existing clients keep working.
  • New backend id hidmaestro with error codes DRIVER_MISSING / HELPER_MISSING; new requires code "hidmaestro>=1.7". Windows catalog now offers ids 2 (dualsense) and 3 (switchpro) — their art already existed from the Kenney CC0 vendoring.
  • Dashboard setup guide now shows remediation for every unavailable backend, with new in-set driver-kernel / driver-user glyphs; backend.hidmaestro.* strings added to all six locales.

Installer / packaging / CI

  • New optional-but-default "HIDMaestro driver" component: deploys at setup via satellite-hm-helper.exe install-driver (idempotent, no reboot). /HIDMAESTRO=auto|bundled|skip and /REMOVEHIDMAESTRO=yes|no|auto mirror the ViGEmBus switches; uninstall prompts separately per driver (default No, shared-driver warning).
  • fetch-redist.ps1 pins HIDMaestro-v1.7.0.zip by SHA-256 and stages the SDK assemblies for the helper build; redist/README.md, SBOM, SECURITY.md, and VENDORED.md inventories updated.
  • windows-ci and release build the helper via SHA-pinned actions/setup-dotnet; the installer round-trip skips both drivers (/VIGEM=skip /HIDMAESTRO=skip); install-dependencies.bat / build-installer.bat grow the .NET SDK 10 step.

Tests (all green locally: 28/28 ctest, clang-format 22.1.4 clean, core-purity gate OK)

  • test_backend_registry — registry identity, JSON builder, catalog-traits fold (union, preference order, availability independence).
  • test_gamepad_mux — routing, plug fallback, quarantine-preserving unplug, callback fan-in, identity union, motion routing.
  • test_hidmaestro_wire — byte-pinned section layouts, seqlock progression, capacity guards, legacy↔extended mode switch, GIP slice, ring cursor/lap-skip/torn-slot rules. Pure — runs on every CI lane.
  • test_hidmaestro_report — byte-exact packer vectors round-tripped through the XUSB companion's documented decode math; DS4/DS5/Switch layouts incl. IMU rescale, touch, battery; rumble/lightbar decoders with flag gating. Pure — every lane.
  • test_hidmaestro_adapter — the real adapter against a fake provisioner (in-process anonymous mappings): submit bytes, merged motion/touch/battery state, elevation-declined and provision-failure paths, output-ring workers delivering rumble + lightbar, closeBus teardown.
  • Catalog/route suites extended for the new traits and the backends array.

Verified locally

  • Full MinGW build of satellite.exe (warning-free) + all 28 ctest suites.
  • dotnet publish of the helper (warnings-as-errors) and a smoke run.
  • iscc installer.iss compiles the full installer with both drivers bundled.
  • fetch-redist.ps1 end-to-end (download, hash verify, SDK staging).

Not verified here: a live end-to-end run against the real HIDMaestro driver (needs the driver deployed + an elevated helper session); the wire/report layouts are pinned against the v1.7.0 driver sources instead, and the layout tests fail loudly on any future drift.

Follow-up commits after live testing

  • Helper stays resident across idle closes (6db45f7). SessionService closes the backend bus whenever the last controller unplugs; the first cut shut the elevated helper down there too, which would have meant a fresh UAC prompt on every client reconnect. The helper now survives idle periods (still exits with satellite via its parent watch, or in the adapter destructor) — so it is one UAC prompt per satellite session, verified live: the prompt appears on the first HIDMaestro bind from the client.
  • Dashboard update-banner layout fix (aff6994, drive-by). Pre-existing bug surfaced during testing of this build: the banner's four-button actions row is flex-shrink: 0 inside a no-wrap flex row, so narrow windows pushed the buttons off the banner and crushed the body to one character per line. The banner now wraps and the body has a 200px flex basis. Unrelated to the backend work — happy to split it out if preferred.

Adds the HIDMaestro user-mode UMDF2 driver as a second, coexisting
Windows gamepad backend. Windows now offers all four controller types:
DualSense and Switch Pro (both with motion) materialize via HIDMaestro,
while 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; the offered set is the union.

Core (pure, tested on every lane):
- core/backend_registry: data-driven backend identity/vendor/per-type
  feature + latency-tier table; buildBackendsJson() and the catalog
  traits fold both derive from it. Catalog traits gain per-type
  dualsense/switchpro motion fields with their own requires codes.
- core/gamepad_mux: composite IGamepadPort sharing one serial space
  across preference-ordered backends; SessionService keeps its single
  port reference and serial pool untouched.
- core/ds4_report.h: the DS4 v2 codec hoisted from platform/macos so
  the Windows HIDMaestro packer shares it byte-for-byte.

Windows backend (hot path fully native, zero allocations per frame):
- hidmaestro_wire: seqlock input-frame writer (GIP slice, mandatory
  ExtendedReportSize clear) + 64-slot output-ring reader, pinned to the
  v1.7.0 driver layout (no protocol version field upstream, so the
  constants ARE the compatibility contract; see lib/VENDORED.md).
- hidmaestro_report: per-profile packers (xbox-360-wired HID+GIP,
  dualshock-4-v2, dualsense at real hardware offsets incl. IMU/touch/
  battery, switch-pro 0x30 body) and ring-packet rumble/lightbar decode.
  Sony IMU rescaled to the driver's neutral-calibration units.
- hidmaestro_adapter: flat per-serial slots holding mapped views +
  duplicated handles; submit = pack + seqlock memcpy + SetEvent; one
  output-ring worker per plugged serial for rumble/lightbar callbacks.
- helper/hidmaestro (satellite-hm-helper.exe, .NET 10 self-contained):
  the elevated lifecycle broker. Satellite stays asInvoker; the helper
  drives the HIDMaestro SDK (driver deploy, SwDevice + shared-section
  creation) and duplicates section/event handles into the satellite
  process over a private named pipe (client PID verified). One UAC
  prompt per session, only on the first HIDMaestro plug.

API/UI (additive, protocol 1): capabilities + /api/backend/status gain
a `backends` array; the singular `backend` object now reports the
preferred-available backend. Windows catalog offers ids 2 and 3.
Dashboard renders remediation for every unavailable backend (new
driver-kernel/driver-user glyphs), hidmaestro copy in all six locales.

Packaging/CI: optional-but-default "HIDMaestro driver" installer
component with /HIDMAESTRO= and /REMOVEHIDMAESTRO= switches mirroring
the ViGEmBus ones (deploy at setup, no reboot; uninstall prompt kept
separate); fetch-redist pins the HIDMaestro release zip by SHA-256 and
stages the SDK assemblies; windows-ci and release build the helper via
pinned setup-dotnet; installer round-trip skips both drivers; SBOM and
VENDORED.md entries added.

Tests: test_backend_registry, test_gamepad_mux, test_hidmaestro_wire,
test_hidmaestro_report (pure, all lanes), test_hidmaestro_adapter
(fake-provisioner, Windows), plus catalog/route suites extended.
The dashboard banner's four-button actions row is flex-shrink: 0 inside a
no-wrap flex row; below ~700px the actions overflowed off the banner and
the body (the only shrinkable item, flex-basis 0) collapsed to zero width,
wrapping the title/detail one character per line. Let the banner wrap and
give the body a 200px basis so the actions drop to their own row instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant