core: per-profile ViGEm axis routing with unipolar output mode

RioProfile gains a nullable AxisRouting section mapping each calibrated
axis (X/Y/Z/Rx/Ry/Rz) to a pad target (thumbs, triggers, or None) with a
Centered or UnipolarPositive conversion; the default reproduces the old
hardcoded routing exactly, so existing profiles are untouched. Routing
resolution lives in a pure, ViGEm-free AxisRouter for testability; the
sink neutralizes the pad on routing change so stale trigger state cannot
leak across profile switches.

Motivation: Descent reads the pad via SDL GameController, where the
triggers are its stock fire axis-buttons - the old fixed routing put
throttle on LeftTrigger (fires) and detent would have read as full
reverse. descent-d1x.json now routes Z->RightThumbY (UnipolarPositive,
detent = center) and Rz->RightThumbX, triggers untargeted; guarded by
tests that parse the shipped JSON through the real deserializer and
byte-compare the dxx-rebirth reference copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-30 09:00:09 -05:00
co-authored by Claude Fable 5
parent 23dec8901b
commit 2f2438717a
9 changed files with 463 additions and 16 deletions
+15
View File
@@ -206,6 +206,21 @@ Core logic in `src/RioJoy.Core/Profiles` + `RioRuntime`; UI/OS in `src/RioJoy.Tr
- Joystick output now uses the real `HidFeederJoystickSink` when the driver is
present (verified end-to-end); `NullJoystickSink` remains only as the
no-driver fallback.
- **Per-profile ViGEm axis routing — code-complete ✅.** `RioProfile.AxisRouting`
(`Output/AxisRoutingConfig`: one route per calibrated axis = pad target —
four thumbs, two triggers, or None — + output mode; shared types with no
ViGEm references, so net40 keeps compiling) re-routes the six axes on the
Xbox 360 pad. Modes: `Centered` (legacy bipolar) and `UnipolarPositive`
(calibrated 0 → thumb center — for the ratcheted unipolar throttle). Null/
absent = the historical fixed routing, so existing profiles are untouched.
Resolution + conversion is the pure unit-tested `AxisRouter`;
`ViGEmJoystickSink.SetRouting` applies it thread-safely and neutralizes all
thumbs/triggers each profile switch (no stale axes); wired in
`RioCoordinator.Activate` beside the calibrator config. First consumer:
`profiles/descent-d1x.json` (throttle→RightThumbY unipolar, rudder→
RightThumbX, triggers untouched — DXX fires on the LT/RT axis-buttons).
The HID feeder (native 6-axis report) intentionally ignores routing.
⏳ Remaining: on-cabinet throttle/rudder feel check (first Descent flight).
-**Remaining:** full on-cabinet verification of the auto-switch +
acquire/release lifecycle against real RIO hardware.