Repository: LizardByte/Sunshine
Pull request #3738: feat(input/linux): DualSense adaptive trigger support
Author: login: ABeltramo
Created: 2025-03-19T18:10:46Z | Merged: 2025-03-26T23:16:20Z

## Description
This is part of a series of PRs:

### Moonlight client
 - https://github.com/moonlight-stream/moonlight-common-c/pull/102 to extend the Moonlight protocol for this new event
 - https://github.com/moonlight-stream/moonlight-qt/pull/1561 requires the changes in `moonlight-common-c` and exposes the adaptive trigger events via SDL

### Sunshine server
 - https://github.com/games-on-whales/inputtino/pull/26 to expose the adaptive triggers events in Linux
 - https://github.com/LizardByte/Sunshine/pull/3738  requires the changes in `moonlight-common-c` and `inputtino` and delivers the adaptive trigger events to Moonlight clients


### Screenshot
https://github.com/user-attachments/assets/c403a181-bae7-4b79-a5b6-7278f5bba97e


### Issues Fixed or Closed


## Type of Change
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] Dependency update (updates to dependencies)
- [ ] Documentation update (changes to documentation)
- [ ] Repository update (changes to repository files, e.g. `.github/...`)

## Checklist
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have added or updated the in code docstring/documentation-blocks for new or existing methods/components



Diff:
diff --git a/src/platform/common.h b/src/platform/common.h
index 49d1fc7cb19..28704bb128e 100644
--- a/src/platform/common.h
+++ b/src/platform/common.h
@@ -106,6 +106,7 @@ namespace platf {
     rumble_triggers,  ///< Rumble triggers
     set_motion_event_state,  ///< Set motion event state
     set_rgb_led,  ///< Set RGB LED
+    set_adaptive_triggers,  ///< Set adaptive triggers
   };
 
   struct gamepad_feedback_msg_t {
@@ -142,6 +143,14 @@ namespace platf {
       return msg;
     }
 
+    static gamepad_feedback_msg_t make_adaptive_triggers(std::uint16_t id, uint8_t event_flags, uint8_t type_left, uint8_t type_right, const std::array<uint8_t, 10> &left, const std::array<uint8_t, 10> &right) {
+      gamepad_feedback_msg_t msg;
+      msg.type = gamepad_feedback_e::set_adaptive_triggers;
+      msg.id = id;
+      msg.data.adaptive_triggers = {.event_flags = event_flags, .type_left = type_left, .type_right = type_right, .left = left, .right = right};
+      return msg;
+    }
+
     gamepad_feedback_e type;
     std::uint16_t id;
 
@@ -166,6 +175,15 @@ namespace platf {
         std::uint8_t g;
         std::uint8_t b;
       } rgb_led;
+
+      struct {
+        uint16_t controllerNumber;
+        uint8_t event_flags;
+        uint8_t type_left;
+        uint8_t type_right;
+        std::array<uint8_t, 10> left;
+        std::array<uint8_t, 10> right;
+      } adaptive_triggers;
     } data;
   };
 
diff --git a/src/platform/linux/input/inputtino_gamepad.cpp b/src/platform/linux/input/inputtino_gamepad.cpp
index 16ecc56fa4f..438146317c6 100644
--- a/src/platform/linux/input/inputtino_gamepad.cpp
+++ b/src/platform/linux/input/inputtino_gamepad.cpp
@@ -152,6 +152,10 @@ namespace platf::gamepad {
               gamepad->last_rgb_led = msg;
             });
 
+            (*ds5).set_on_trigger_effect([feedback_queue, idx = id.clientRelativeIndex](const inputtino::PS5Joypad::TriggerEffect &trigger_effect) {
+              feedback_queue->raise(gamepad_feedback_msg_t::make_adaptive_triggers(idx, trigger_effect.event_flags, trigger_effect.type_left, trigger_effect.type_right, trigger_effect.left, trigger_effect.right));
+            });
+
             // Activate the motion sensors
             feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_ACCEL, 100));
             feedback_queue->raise(gamepad_feedback_msg_t::make_motion_event_state(id.clientRelativeIndex, LI_MOTION_TYPE_GYRO, 100));
diff --git a/src/stream.cpp b/src/stream.cpp
index b42bd3049f5..2572d555b38 100644
--- a/src/stream.cpp
+++ b/src/stream.cpp
@@ -48,6 +48,7 @@ extern "C" {
 #define IDX_RUMBLE_TRIGGER_DATA 12
 #define IDX_SET_MOTION_EVENT 13
 #define IDX_SET_RGB_LED 14
+#define IDX_SET_ADAPTIVE_TRIGGERS 15
 
 static const short packetTypes[] = {
   0x0305,  // Start A
@@ -65,6 +66,7 @@ static const short packetTypes[] = {
   0x5500,  // Rumble triggers (Sunshine protocol extension)
   0x5501,  // Set motion event (Sunshine protocol extension)
   0x5502,  // Set RGB LED (Sunshine protocol extension)
+  0x5503,  // Set Adaptive triggers (Sunshine protocol extension)
 };
 
 namespace asio = boost::asio;
@@ -186,6 +188,21 @@ namespace stream {
     std::uint8_t b;
   };
 
+  struct control_adaptive_triggers_t {
+    control_header_v2 header;
+
+    std::uint16_t id;
+    /**
+     * 0x04 - Right trigger
+     * 0x08 - Left trigger
+     */
+    std::uint8_t event_flags;
+    std::uint8_t type_left;
+    std::uint8_t type_right;
+    std::uint8_t left[DS_EFFECT_PAYLOAD_SIZE];
+    std::uint8_t right[DS_EFFECT_PAYLOAD_SIZE];
+  };
+
   struct control_hdr_mode_t {
     control_header_v2 header;
 
@@ -833,6 +850,22 @@ namespace stream {
       plaintext.b = data.b;
 
       BOOST_LOG(verbose) << "RGB: "sv << msg.id << " :: "sv << util::hex(data.r).to_string_view() << util::hex(data.g).to_string_view() << util::hex(data.b).to_string_view();
+      std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
+        encrypted_payload;
+
+      payload = encode_control(session, util::view(plaintext), encrypted_payload);
+    } else if (msg.type == platf::gamepad_feedback_e::set_adaptive_triggers) {
+      control_adaptive_triggers_t plaintext;
+      plaintext.header.type = packetTypes[IDX_SET_ADAPTIVE_TRIGGERS];
+      plaintext.header.payloadLength = sizeof(plaintext) - sizeof(control_header_v2);
+
+      plaintext.id = util::endian::little(msg.id);
+      plaintext.event_flags = msg.data.adaptive_triggers.event_flags;
+      plaintext.type_left = msg.data.adaptive_triggers.type_left;
+      std::ranges::copy(msg.data.adaptive_triggers.left, plaintext.left);
+      plaintext.type_right = msg.data.adaptive_triggers.type_right;
+      std::ranges::copy(msg.data.adaptive_triggers.right, plaintext.right);
+
       std::array<std::uint8_t, sizeof(control_encrypted_t) + crypto::cipher::round_to_pkcs7_padded(sizeof(plaintext)) + crypto::cipher::tag_size>
         encrypted_payload;
 
diff --git a/third-party/inputtino b/third-party/inputtino
index 1170b1e4ad0..fd136cfe492 160000
--- a/third-party/inputtino
+++ b/third-party/inputtino
@@ -1 +1 @@
-Subproject commit 1170b1e4ad0f987b24dc94b20c0e866e8350b741
+Subproject commit fd136cfe492b4375b4507718bcca1f044588fc6f


Reviews:
user: login: ReenigneArcher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:45:11Z
user: login: ABeltramo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T21:51:51Z
user: login: ReenigneArcher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:58:06Z
user: login: ReenigneArcher
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T22:12:38Z
user: login: ABeltramo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T22:14:36Z

Review comments:
path: third-party/moonlight-common-c
user: login: ReenigneArcher
body: I think we're already on the correct commit? (The inputtino submodule seems correct though)
created_at: 2025-03-26T21:40:18Z
author_association: MEMBER
path: third-party/moonlight-common-c
user: login: ABeltramo
body: I'm not sure what the rebase did since it worked out of the box, it seems to be pointing to the right commit though, or am I missing something?
created_at: 2025-03-26T21:51:51Z
author_association: CONTRIBUTOR
path: third-party/moonlight-common-c
user: login: ReenigneArcher
body: I'm confused because it's saying this is the change: https://github.com/moonlight-stream/moonlight-common-c/compare/e95feaf4951b8dc774671a5d6a1c31d76d78e3ac...386b9684f2e0087c59c99f8b5695972de2e87a1c

But we're already on e95feaf4951b8dc774671a5d6a1c31d76d78e3ac

Perhaps just one commit needs to be removed?

![image](https://github.com/user-attachments/assets/e04ea5b7-4ba7-4b5b-8062-872216cffd51)


created_at: 2025-03-26T21:58:06Z
author_association: MEMBER
path: third-party/moonlight-common-c
user: login: ReenigneArcher
body: So, it's trying to change from e95feaf to 386b968.

This commit doesn't exist in the repo. https://github.com/moonlight-stream/moonlight-common-c/commit/386b9684f2e0087c59c99f8b5695972de2e87a1c Probably it was a commit in your PR, but got squashed when it was merged?
created_at: 2025-03-26T22:12:38Z
author_association: MEMBER
path: third-party/moonlight-common-c
user: login: ABeltramo
body: Let me just pop this commit, it's just easier this way
created_at: 2025-03-26T22:14:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: ABeltramo
created_at: 2025-03-26T17:57:32Z
body: I've updated `moonlight-common-c` and `inputtino` to bring in the merged changes, this should be ready for review.

If you need a quick way to test this out with a DualSense joypad you can use https://github.com/games-on-whales/sdl-jstest. After building, you can just call
```
./sdl2-jstest --gamecontroller 0
```
during a streaming session and by pressing the microphone button, it'll cycle through a couple of different trigger effects.  
(that is what I use on the video above :wink:)
author_association: CONTRIBUTOR
user: login: ReenigneArcher
created_at: 2025-03-26T20:22:51Z
body: @ABeltramo Can you resolve the conflicts so the workflows will run? It's probably moonlight-common-c because I already merged the dependabot PR for it.
author_association: MEMBER
user: login: ABeltramo
created_at: 2025-03-26T20:36:27Z
body: @ReenigneArcher should be good now. Is there any way to ignore the 3 SonarQube issues? They don't really apply to this code.
author_association: CONTRIBUTOR
user: login: ReenigneArcher
created_at: 2025-03-26T20:41:00Z
body: I'm not enforcing sonar. If it doesn't apply that's fine
author_association: MEMBER
user: login: codecov[bot]
created_at: 2025-03-26T20:52:27Z
body: ## [Codecov](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) Report
Attention: Patch coverage is `0%` with `21 lines` in your changes missing coverage. Please review.
> Project coverage is 11.59%. Comparing base [(`c4d991c`)](https://app.codecov.io/gh/LizardByte/Sunshine/commit/c4d991c8f7cc758a6c4e62c03f202fb0f46d1788?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) to head [(`71d5ac4`)](https://app.codecov.io/gh/LizardByte/Sunshine/commit/71d5ac46b0f4b69abb5e402ca2813fd40f3eee13?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte).

| [Files with missing lines](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) | Patch % | Lines |
|---|---|---|
| [src/stream.cpp](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&filepath=src%2Fstream.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte#diff-c3JjL3N0cmVhbS5jcHA=) | 0.00% | [11 Missing and 1 partial :warning: ](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) |
| [src/platform/common.h](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&filepath=src%2Fplatform%2Fcommon.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte#diff-c3JjL3BsYXRmb3JtL2NvbW1vbi5o) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) |
| [src/platform/linux/input/inputtino\_gamepad.cpp](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&filepath=src%2Fplatform%2Flinux%2Finput%2Finputtino_gamepad.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte#diff-c3JjL3BsYXRmb3JtL2xpbnV4L2lucHV0L2lucHV0dGlub19nYW1lcGFkLmNwcA==) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3738      +/-   ##
==========================================
- Coverage   11.61%   11.59%   -0.02%     
==========================================
  Files          92       92              
  Lines       17337    17358      +21     
  Branches     8100     8106       +6     
==========================================
  Hits         2013     2013              
- Misses      12833    12853      +20     
- Partials     2491     2492       +1     
```

| [Flag](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) | Coverage Δ | |
|---|---|---|
| [Linux](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) | `11.25% <0.00%> (-0.03%)` | :arrow_down: |
| [Windows](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) | `13.00% <0.00%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte) | Coverage Δ | |
|---|---|---|
| [src/platform/linux/input/inputtino\_gamepad.cpp](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&filepath=src%2Fplatform%2Flinux%2Finput%2Finputtino_gamepad.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte#diff-c3JjL3BsYXRmb3JtL2xpbnV4L2lucHV0L2lucHV0dGlub19nYW1lcGFkLmNwcA==) | `2.50% <0.00%> (-0.05%)` | :arrow_down: |
| [src/platform/common.h](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&filepath=src%2Fplatform%2Fcommon.h&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte#diff-c3JjL3BsYXRmb3JtL2NvbW1vbi5o) | `23.65% <0.00%> (-1.64%)` | :arrow_down: |
| [src/stream.cpp](https://app.codecov.io/gh/LizardByte/Sunshine/pull/3738?src=pr&el=tree&filepath=src%2Fstream.cpp&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=LizardByte#diff-c3JjL3N0cmVhbS5jcHA=) | `2.47% <0.00%> (-0.04%)` | :arrow_down: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-26T22:30:43Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=LizardByte_Sunshine&pullRequest=3738) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [3 New issues](https://sonarcloud.io/project/issues?id=LizardByte_Sunshine&pullRequest=3738&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [3 New Code Smells](https://sonarcloud.io/dashboard?id=LizardByte_Sunshine&pullRequest=3738) (required ≤ 0)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=LizardByte_Sunshine&pullRequest=3738)

##   
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/light_bulb-16px.png '') Catch issues before they fail your Quality Gate with our IDE extension ![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/sonarlint-16px.png '') [SonarQube for IDE](https://www.sonarsource.com/products/sonarlint/features/connected-mode/?referrer=pull-request)


author_association: NONE