From 00822d65561d2955f04a52baa2c2687bdb034ada Mon Sep 17 00:00:00 2001 From: MarcelineVPQ Date: Sun, 7 Jun 2026 10:44:31 -0600 Subject: [PATCH] docs: document native adaptive triggers in games (Linux/Proton) + the descriptor/feature-data fixes CHANGELOG: headline + Added/Changed/Known-tradeoff for the native-trigger work. README: a Features bullet, a "Linux: native adaptive triggers (Proton)" setup section with every launch option in a reference table, and a caveat that the WebHID Config/Remap tabs are disabled on this firmware. CLAUDE.md: a load-bearing gotcha -- the HID descriptor must stay byte-identical to a real DS5 (289 bytes) and feature reports 0x09/0x20/0x05 must serve the real cached controller data, or native game triggers silently break. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 17 +++++++++++++++++ CLAUDE.md | 2 ++ README.md | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e93d25..f08fc28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ## [Unreleased] +Headline: **native DualSense adaptive triggers now fire in real PC games on Linux/Proton — through the dongle, 1:1 with a wired controller.** For weeks the triggers only ever worked in the on-dongle OLED self-test, never in a game: the dongle was *recognised*, but games fell back to a generic/Xbox pad and never sent trigger effects. The root cause was three ways the dongle's USB presentation differed from genuine Sony hardware — each one accepted by Linux's `hid_playstation` (which only checks size + CRC) but **rejected by a game's native DualSense detection, which validates the actual content**. Closing all three makes the dongle byte-for-byte indistinguishable from a real DS5 to the game. + +**Host-side recipe (required alongside this firmware):** run the game on a Proton that carries the Wine `winebus.sys` **#9034** fix (Wine 11 / current Proton-GE — the bug suppresses the SDL gamepad device when a hidraw device exists for the same VID/PID, so the pad shows up everywhere *except* in-game), set `PROTON_ENABLE_HIDRAW=0x054c/0x0ce6` in the launch options, and **disable Steam Input** (native path). With a real DS5 this "just works"; the firmware changes below make the dongle match. + +### Added + +- **Native adaptive triggers & haptics in games on Linux/Proton, through the dongle.** Games with native DualSense support (Cyberpunk 2077, etc.) now drive the controller's adaptive triggers via the dongle, identical to a directly-wired DualSense — verified 1:1 by A/B against the same controller plugged in over USB-C. No Steam Input, no per-game hacks beyond the host recipe above. The firmware already proxies the host's trigger output reports to the controller (unchanged); the missing piece was getting games to *recognise* the dongle as a genuine DS5 in the first place (below). + +### Changed + +- **The DualSense HID report descriptor is now byte-identical to a real DS5 (289 bytes, was 321).** The OLED Edition declared four extra feature reports — `0xF6`–`0xF9`, the WebHID config/remap IDs — inside the gamepad collection, making the descriptor 32 bytes longer than genuine hardware. Games' native DualSense parser rejects that mismatch and falls back to a generic pad (no triggers); `hid_playstation` didn't care, which is why the OLED self-test always worked but games never did. The four declarations are removed (plus the runtime `wDescriptorLength` patch that overrode the static array, `0x41`→`0x21`). The firmware **still handles** `0xF6`–`0xF9`; they're simply undeclared now and work over Linux `hidraw` exactly like the existing `0xFD` diagnostic report. +- **Feature reports `0x09` (pairing), `0x20` (firmware) and `0x05` (calibration) now return the controller's real cached data, not zeros.** The earlier native-recognition fix answered these with zeros + a valid CRC — enough for `hid_playstation` to bind, but games validate the *content*: Cyberpunk GET-read `0x20`/`0x09` ~156× in a retry storm rejecting the zeros, never completed the DualSense handshake, and showed no controller. The dongle already fetches and caches the genuine reports from the connected controller over BT (`init_feature()`); `tud_hid_get_report_cb` now serves that cache (real firmware string / pairing / calibration), keeping the CRC-valid synthetic stub only as a fallback for the USB-enumeration probe before the BT link is up (so `hid_playstation` still binds then). Builds on the `hid_playstation` binding fix and the serial-=-MAC identity fix. + +### Known tradeoff + +- **The browser (WebHID) Config and Remap tabs no longer reach the dongle.** They drove config over reports `0xF6`/`0xF7`, which are no longer *declared* in the HID descriptor — and WebHID refuses undeclared report IDs (declaring them is exactly what broke game triggers). Mitigations, no firmware change needed: **on-dongle OLED config still works**, and because undeclared IDs are fine over Linux `hidraw`, a small `hidraw` CLI/script can read & write all of `0xF6`–`0xF9` (the same mechanism `scripts/mic_diag.sh` uses for `0xFD`). Restoring the *browser* tool would require moving the custom reports into a separate vendor HID collection — which risks the descriptor diverging from a real DS5 again, re-breaking triggers, so it's deliberately not done here. + --- ## [0.6.10-oled-edition] — 2026-05-25 diff --git a/CLAUDE.md b/CLAUDE.md index 74d92fd..027d062 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,6 +97,8 @@ These are non-obvious from the code; they cost time when forgotten. - **Inactivity-disconnect uses `packet[3..12]`** in the L2CAP interrupt data path (`src/bt.cpp:l2cap_packet_handler`). It's looking at sticks and DPad/buttons to decide "idle." Don't touch those bytes' layout without updating the heuristic. - **The 0x36 BT audio packet layout is load-bearing — speaker + HD haptic actuators silently die without the SetStateData sub-report.** Upstream commit `3a31bd7` (May 2026, "refactor: add SetStateData and audio send priority") moved the `0x10` SetStateData block out of every audio frame and into a one-time L2CAP-open setup. The DualSense hardware requires that sub-report (specifically the `0x7f 0x7f` Headphones + Speaker volume bytes) at `pkt[11..75]` of every `0x36` frame, or the actuators stop producing output even though USB and BT byte counts look fine. Our fork keeps `state_data[63]` in `src/audio.cpp` and re-asserts it on every frame; the pre-3a31bd7 packet layout is: state_data at `pkt[11..75]`, haptic at `pkt[76..141]`, speaker format at `pkt[142]`, opus payload at `pkt[144..343]`. If you rebase onto an upstream that has the refactor and don't preserve this restoration, speaker + HD haptics silently break. The `scripts/test_speaker.sh` helper + the `USB aud / BT 0x32` counters on the OLED Diagnostics screen are the regression tripwire — if bytes are flowing but you hear nothing, look at packet contents not flow. Upstream PR #93 is tracking a proper unified fix; ours is the pre-refactor revert applied just to `audio.cpp`. Same fix was shipped independently by `loteran/DS5Dongle` (commit `c7a8d3c`). +- **The DualSense HID descriptor must stay byte-identical to a real DS5 (289 bytes), and `0x09`/`0x20`/`0x05` feature reports must return the controller's *real* cached data — or native game triggers silently break.** A game's native DualSense detection (Cyberpunk, etc.) validates *both* the descriptor shape and the feature-report *content*, then drives adaptive triggers over the native hidraw path. If either differs from genuine hardware the game falls back to a generic/Xbox pad and triggers never fire. Linux's `hid_playstation` is far more lenient (size + CRC only), so the dongle still *binds* and the on-dongle OLED trigger-test still works — which masked this break for weeks. Two load-bearing things: (1) **don't re-declare config reports `0xF6`–`0xF9`** in `desc_hid_report_ds` (or bump its `wDescriptorLength` runtime patch in `tud_descriptor_configuration_cb` back to `0x41`) — that's what made the descriptor 321 bytes and killed triggers; (2) keep `tud_hid_get_report_cb` serving the **real `init_feature()` cache** for `0x09`/`0x20`/`0x05`, with the CRC-valid synthetic stub only as the pre-BT-link probe fallback. `desc_hid_report_dse` (DSE mode) still declares F6-F9 and needs the same cut. Host side requires a Proton with the Wine `winebus.sys` #9034 fix + `PROTON_ENABLE_HIDRAW=0x054c/0x0ce6` + Steam Input off; diff a dongle vs real-DS5 `PROTON_LOG=+hid` capture to debug (a GET retry storm on `0x20`/`0x09` = the dongle's feature content is being rejected). + ## Versioning — single source of truth The release tag is the **only** place the version is written. Everything else flows from it: diff --git a/README.md b/README.md index 3cf5da2..ba8e44d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ The web tool is a one-stop shop — **no installs, no command line, no `picotool Works in any Chromium-based browser (Chrome, Edge, Brave, Opera). Firefox + Safari don't expose WebHID or WebUSB, so flashing and live config aren't available there — the OLED Preview still renders with mock data. +> **⚠️ Native-trigger firmware (Unreleased) and the WebHID tabs.** To make games on Linux/Proton recognise the dongle as a genuine DualSense and fire **native adaptive triggers** (see [CHANGELOG.md](./CHANGELOG.md)), the firmware's HID descriptor is now byte-identical to a real DS5 — which meant *un-declaring* the `0xF6`/`0xF7` config reports the browser **Config** and **Remap** tabs depend on. On that firmware those two tabs can't reach the dongle (WebHID refuses undeclared report IDs). **Flash Firmware and OLED Preview still work**, on-dongle OLED config still works, and the same settings can be driven over Linux `hidraw`. Earlier releases are unaffected. + > Source for the web tool: **[MarcelineVPQ/DS5Dongle-OLED-Config-Web](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Config-Web)** (fork of [awalol/ds5dongle-config-web](https://github.com/awalol/ds5dongle-config-web)). --- @@ -43,6 +45,7 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f **OLED Edition additions:** +- **Native DualSense adaptive triggers in PC games on Linux/Proton — through the dongle.** Games with native DualSense support drive the controller's adaptive triggers wirelessly via the dongle, **1:1 with a directly-wired DualSense**. Requires a Proton carrying the Wine `winebus.sys` #9034 fix (Wine 11 / current Proton-GE), `PROTON_ENABLE_HIDRAW=0x054c/0x0ce6`, and Steam Input disabled; the firmware makes the dongle byte-for-byte indistinguishable from a real DS5 so the game accepts it (full write-up in [CHANGELOG.md](./CHANGELOG.md)). Trade-off: the browser Config/Remap tabs are disabled on this firmware (see the note above). - Optional Pico-OLED-1.3 status display with **11 screens** (status, slots, lightbar, trigger test, gyro tilt, touchpad, diagnostics, CPU/clock, RSSI, VU meters, settings) - **Button remapping** — reassign any of the 16 digital controls (face buttons, D-pad, shoulders/triggers, stick clicks, Create/Options) to any other. Stored on the dongle and applied before the host sees the report, so it works in **every game and OS with no host-side software**; identity (no remap) is the default. Edit it visually in the [web config tool](#️-web-config-tool)'s **Remap tab**, or headlessly via `scripts/remap_test.py`. Persisted in its own flash sector, survives reboot. - **4-slot persistent multi-controller pairing** — bond up to four DualSenses, switch between them from the OLED, slot 0 reconnects automatically on boot @@ -52,6 +55,42 @@ This project enables the Raspberry Pi Pico2W to function as a Bluetooth bridge f - **Soft-reboot** without unplugging USB via DS5 `PS + Mute` hold (works headless) or **KEY0 + KEY1 held together for 1 s** on the OLED add-on (replaces the older KEY0 double-click gesture, which was easy to fire by accident while paging quickly) - **Audit pass on the core bridge** — critical stack-overflow fix in the audio path (resolves long-standing "audio stuttering"), security hardening, watchdog, length validation across HID/L2CAP boundaries (see [CHANGELOG.md](./CHANGELOG.md)) +## 🐧 Linux: native adaptive triggers (Proton) + +Getting a game to drive the controller's **adaptive triggers through the dongle** on Linux needs a Proton that carries the Wine `winebus.sys` **#9034** fix. **As of this writing no official Proton-GE release includes it yet** — it's merged upstream but unreleased, and `GE-Proton10-34` (the current release) and earlier do **not** have it. So a build that does is provided: **`GE-Proton-DualSense`** (see [Releases](https://github.com/MarcelineVPQ/DS5Dongle-OLED-Edition/releases)). Once an official GE-Proton ships the fix, that will work too. + +> Why: the #9034 bug suppresses the SDL gamepad device when a hidraw device exists for the same VID/PID, so the pad is seen *everywhere except in-game*. The fix lets the native (trigger-capable) path and a working gamepad coexist. + +**One-time setup** + +1. **Install the Proton build.** Download `GE-Proton-DualSense.tar.gz` and extract it to: + - **Steam:** `~/.steam/root/compatibilitytools.d/` + - **Heroic:** `~/.config/heroic/tools/proton/` + + Then **fully restart Steam** so it detects the new compatibility tool. +2. **Disable Steam Input** so the game talks to the controller natively — Steam → game **Properties → Controller → Disable Steam Input** (or globally: Settings → Controller → turn off PlayStation Controller support). + +**Per game** + +3. **Force the Proton:** Steam → right-click the game → **Properties → Compatibility → Force the use of a specific Steam Play compatibility tool → `GE-Proton-DualSense`**. (Heroic: set the game's *Wine version* to it.) +4. **Set the launch option** (Steam: Properties → General → Launch Options; Heroic: per-game Environment Variables): + + ``` + PROTON_ENABLE_HIDRAW=0x054c/0x0ce6 %command% + ``` + +Launch — native adaptive triggers fire through the dongle, 1:1 with a wired DualSense. The game must have native DualSense support (e.g. Cyberpunk 2077). First launch under a new Proton does locale/prefix setup and can sit "Not Responding" for a minute — that's setup, not a crash; let it finish. + +**Launch-option reference** + +| Variable | Value | Purpose | +|---|---|---| +| `PROTON_ENABLE_HIDRAW` | `0x054c/0x0ce6` | **Required for triggers.** Exposes the dongle's raw HID to the game so it uses the native DualSense path (adaptive triggers + haptics). `054c/0ce6` is the dongle's USB VID/PID — identical to a real DualSense. | +| `PROTON_PREFER_SDL` | `1` | *Alternative, input-only.* Forces the SDL gamepad path — gives working input + rumble but as a generic/Xbox pad (**no triggers**). Use only when a game lacks native DualSense support and `ENABLE_HIDRAW` doesn't help. Do **not** combine with `ENABLE_HIDRAW` for trigger games. | +| `PROTON_LOG` | `+hid,+setupapi,+plugplay` | *Diagnostic only* (not for normal play). Writes a HID enumeration trace to `~/steam-.log` — useful for debugging recognition (Steam honors it; Heroic overrides `PROTON_LOG_DIR`/`WINEDEBUG`). | + +> **Heroic vs Steam:** Heroic games launch Proton without Steam's controller layer, so they're simpler — just set the runner to `GE-Proton-DualSense` and the `PROTON_ENABLE_HIDRAW` env var. Steam games additionally need Steam Input disabled (above). + ## Hardware ### Required