feat(dualsense): Phase C/D/E — virtual DualSense routing + 0xCC/0xCD planes + C ABI
ci / rust (push) Has been cancelled

PUNKTFUNK_GAMEPAD=dualsense now routes a session's gamepad through a real virtual
DualSense (UHID + hid-playstation) end to end:

- host: a `PadBackend` enum (m3.rs) selects `GamepadManager` (uinput xpad, default)
  or the new `DualSenseManager` (dualsense.rs) per session. The manager keeps each
  pad's full DsState so touchpad + motion (rich-input plane) persist across
  button/stick frames, and services the !Send /dev/uhid fd only on the input thread
  (which cycles <=4ms, so the GET_REPORT init handshake completes).
- feedback: `service()` now returns `DsFeedback { hidout, rumble }`. Motor rumble
  stays on the universal 0xCA plane (so non-DualSense clients still feel it; manager
  dedups change); lightbar / player LEDs / adaptive-trigger effects ride the new
  0xCD HID-output plane (host->client) as `HidOutput`.
- rich input: touchpad contacts + motion ride the 0xCC plane (client->host) as
  `RichInput`, applied via `DualSenseManager::apply_rich` (merged with button state;
  touch normalized 0..65535 -> the touchpad resolution).
- connector + C ABI: `NativeClient::next_hidout` / `send_rich_input`, exported as
  `punktfunk_connection_next_hidout` (-> PunktfunkHidOutput) and
  `punktfunk_connection_send_rich_input` (<- PunktfunkRichInput); header regenerated.
- reference client: `--rich-input-test` drives the DualSense touchpad + motion and
  logs the 0xCD feedback that comes back.

Validated live on-box: a synthetic-source m3-host + client-rs created the real
kernel DualSense, drove 0xCC, and decoded 12 live 0xCD events (the kernel's actual
lightbar/trigger init reports) with the data plane unaffected (600/600 frames).
Adversarial review fixes folded in: the input loop no longer skips the rich drain +
feedback pump on a dropped gamepad event, and the touch contact id is clamped to its
slot. Remaining: the Apple client renders triggers/rumble on a real DualSense.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 08:36:12 +00:00
parent e5b15353c7
commit 59edeedf07
8 changed files with 799 additions and 47 deletions
+95
View File
@@ -19,6 +19,24 @@
// added `punktfunk_pair` / `punktfunk_generate_identity` / `punktfunk_connection_request_mode`.
#define ABI_VERSION 2
// `PunktfunkHidOutput::kind` — lightbar RGB (`r`/`g`/`b` valid).
#define PUNKTFUNK_HIDOUT_LED 1
// `PunktfunkHidOutput::kind` — player-indicator LEDs (`player_bits` valid, low 5 bits).
#define PUNKTFUNK_HIDOUT_PLAYER_LEDS 2
// `PunktfunkHidOutput::kind` — one adaptive-trigger effect (`which` + `effect`/`effect_len` valid).
#define PUNKTFUNK_HIDOUT_TRIGGER 3
// Capacity of `PunktfunkHidOutput::effect` (the DualSense trigger parameter block).
#define PUNKTFUNK_HID_EFFECT_MAX 11
// `PunktfunkRichInput::kind` — a touchpad contact (`finger`/`active`/`x`/`y` valid).
#define PUNKTFUNK_RICH_TOUCHPAD 1
// `PunktfunkRichInput::kind` — a motion sample (`gyro`/`accel` valid).
#define PUNKTFUNK_RICH_MOTION 2
// Compositor preference for [`punktfunk_connect_ex`] (`compositor` arg). `AUTO` lets the host
// pick (auto-detect from its running desktop); a concrete value is honored only if that backend
// is available on the host right now, else the host falls back to auto-detect. The resolved
@@ -319,6 +337,57 @@ typedef struct {
} PunktfunkAudioPacket;
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// One DualSense HID-output feedback event a game wrote to the host's virtual pad
// ([`punktfunk_connection_next_hidout`]). `kind` selects which fields are meaningful — replay it
// on a real DualSense (lightbar color, player LEDs, or an adaptive-trigger effect via the
// platform's `GCDualSenseAdaptiveTrigger`-style API).
typedef struct {
// One of `PUNKTFUNK_HIDOUT_*`.
uint8_t kind;
// Gamepad index.
uint8_t pad;
// LED: lightbar red.
uint8_t r;
// LED: lightbar green.
uint8_t g;
// LED: lightbar blue.
uint8_t b;
// PlayerLeds: lit player indicators (low 5 bits).
uint8_t player_bits;
// Trigger: 0 = L2, 1 = R2.
uint8_t which;
// Trigger: number of valid bytes in `effect` (≤ `PUNKTFUNK_HID_EFFECT_MAX`).
uint8_t effect_len;
// Trigger: the raw DualSense trigger parameter block (mode + params).
uint8_t effect[11];
} PunktfunkHidOutput;
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// One rich client→host input for the host's virtual DualSense
// ([`punktfunk_connection_send_rich_input`]): a touchpad contact or a motion sample. Set `kind`
// and the matching fields; the others are ignored.
typedef struct {
// One of `PUNKTFUNK_RICH_*`.
uint8_t kind;
// Gamepad index.
uint8_t pad;
// Touchpad: contact id (0 or 1).
uint8_t finger;
// Touchpad: 1 = finger down, 0 = lifted.
uint8_t active;
// Touchpad: normalized x, 0..=65535 across the touchpad.
uint16_t x;
// Touchpad: normalized y, 0..=65535 across the touchpad.
uint16_t y;
// Motion: gyro (pitch, yaw, roll), raw signed-16.
int16_t gyro[3];
// Motion: accelerometer (x, y, z), raw signed-16.
int16_t accel[3];
} PunktfunkRichInput;
#endif
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
@@ -528,6 +597,20 @@ PunktfunkStatus punktfunk_connection_next_rumble(PunktfunkConnection *c,
uint32_t timeout_ms);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Pull the next DualSense HID-output feedback event (lightbar / player LEDs / adaptive trigger)
// the host's virtual pad received from a game, into `*out`. [`PunktfunkStatus::NoFrame`] on
// timeout, [`PunktfunkStatus::Closed`] once the session ended. Only the DualSense host backend
// emits these. Same threading rules as [`punktfunk_connection_next_rumble`] (one puller, may run
// alongside the other planes).
//
// # Safety
// `c` is a valid connection handle; `out` is writable for one `PunktfunkHidOutput`.
PunktfunkStatus punktfunk_connection_next_hidout(PunktfunkConnection *c,
PunktfunkHidOutput *out,
uint32_t timeout_ms);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Send one input event to the host as a QUIC datagram (non-blocking enqueue).
//
@@ -552,6 +635,18 @@ PunktfunkStatus punktfunk_connection_send_mic(PunktfunkConnection *c,
uint64_t pts_ns);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// Send one rich input event (DualSense touchpad contact or motion sample) to the host as a QUIC
// datagram (non-blocking enqueue). The host applies it to its virtual DualSense pad — a no-op
// unless the host runs the DualSense gamepad backend. [`PunktfunkStatus::InvalidArg`] on an
// unknown `kind`.
//
// # Safety
// `c` is a valid connection handle; `rich` points to a valid [`PunktfunkRichInput`].
PunktfunkStatus punktfunk_connection_send_rich_input(PunktfunkConnection *c,
const PunktfunkRichInput *rich);
#endif
#if defined(PUNKTFUNK_FEATURE_QUIC)
// The currently active session mode — the Welcome's, until an accepted
// [`punktfunk_connection_request_mode`] switches it. Safe any time after connect.