Skip to content

Add support for DualSense Adaptive Triggers and custom joypad data packets#111682

Draft
Nintorch wants to merge 1 commit into
godotengine:masterfrom
Nintorch:dualsense-adaptive-triggers
Draft

Add support for DualSense Adaptive Triggers and custom joypad data packets#111682
Nintorch wants to merge 1 commit into
godotengine:masterfrom
Nintorch:dualsense-adaptive-triggers

Conversation

@Nintorch
Copy link
Copy Markdown
Member

@Nintorch Nintorch commented Oct 15, 2025

Partially addresses godotengine/godot-proposals#12087

Partially supersedes #88590
Requires #111707 to be merged first

I decided to split my big SDL3 joypad features PR ( #107967 ) into several smaller PRs, this PR is one of them.

This PR adds the ability to use DualSense's Adaptive Triggers functionality. This code has been tested in my older PR, see: #107967 (comment), #107967 (comment)

This code was based on this SDL2 Adaptive Triggers example: https://github.com/libsdl-org/SDL/blob/f201b64ffe1380713b181a3421c7fd910c95aada/test/testgamecontroller.c#L304-L352
Here's how the joypad packets are constructed: https://gist.github.com/Nielk1/6d54cc2c00d2201ccb8c2720ad7538db

(I hope I'm not spamming the PRs or anything 😅 )

TODO:

  • Return false if the controller is not DualSense
    Get trigger status (see https://controllers.fandom.com/wiki/Sony_DualSense#Input_Reports and DualSense Adaptive Triggers andrei-drexler/ironwail#309 ) (I think it can be done in a separate PR if needed)
  • replace [code skip-lint]...[/code] with [parameter ...]
  • More documentation using the gist's comments and Apple documentation
  • An option to modify both triggers in one call
  • Input.has_joy_adaptive_triggers()
  • Mark as experimental
  • Don't expose Input.send_joy_packet() (will be superseded by exposing HIDAPI)
  • Change return type to void for most method
  • Allow method calls to be ignored if joypad input is disabled on unfocused application

@Nintorch Nintorch requested review from a team as code owners October 15, 2025 16:07
@AThousandShips AThousandShips added this to the 4.x milestone Oct 15, 2025
@Nintorch Nintorch changed the title Add support for DualSense Adaptive Triggers Add support for DualSense Adaptive Triggers and custom joypad data packets Oct 15, 2025
@Nintorch Nintorch force-pushed the dualsense-adaptive-triggers branch from bc37d56 to 50140bb Compare October 16, 2025 06:32
@xDShot
Copy link
Copy Markdown

xDShot commented Oct 16, 2025

Would be nice if we had HID input reading so we could determine if the triggers are actually affected.

@Nintorch
Copy link
Copy Markdown
Member Author

Would be nice if we had HID input reading so we could determine if the triggers are actually affected.

Not sure it's worth it, I think if the packets are constructed correctly, then the triggers must be affected.
And it would also require deeper understanding of how SDL reads joysticks' hidapi reports and how to read it ourselves 😅

@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Oct 16, 2025

I'll have to double check but my recollection is that you can read the triggers' analog values (similar to how you'd read a stick's axis value) and see the amount it changes by as you push down the triggers change.

@xDShot
Copy link
Copy Markdown

xDShot commented Oct 16, 2025

I'll have to double check but my recollection is that you can read the triggers' analog values (similar to how you'd read a stick's axis value) and see the amount it changes by as you push down the triggers change.

Apart of that, it's possible to read currently applied effect and trigger pressure status, for ex. "Weapon" mode has three statuses (0 not pressed, 1 cocking trigger, 2 is fired)

Would be nice if we had HID input reading so we could determine if the triggers are actually affected.

Not sure it's worth it, I think if the packets are constructed correctly, then the triggers must be affected. And it would also require deeper understanding of how SDL reads joysticks' hidapi reports and how to read it ourselves 😅

Fandom wiki describes input report structure. Also see my implementation for Quake to get the idea:
andrei-drexler/ironwail#309

Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Oct 17, 2025

In between some of my school work, I've been working on making a little demo project to test and demonstrate each of the adaptive trigger methods:

  • joy_adaptive_triggers_feedback
  • joy_adaptive_triggers_multi_feedback
  • joy_adaptive_triggers_multi_vibration
  • joy_adaptive_triggers_off
  • joy_adaptive_triggers_slope_feedback
  • joy_adaptive_triggers_vibration
  • joy_adaptive_triggers_weapon

I can upload the project ZIP when it's done, or post the repo on GitHub so people can use it sooner and then pull updates when I make them (or perhaps contribute portions themselves) 😄


On the TODO, I see you have "Makes the functions only work if the controller is DualSense (throw an error if it's not)". Personally, I feel like it might be better to have the functions do something less intrusive like return false. Given that in most cases, it won't be expected that people are using a DualSense controller, I would expect a lot of these function calls to be wrapped in controller-checking conditionals so that errors aren't thrown:

if Input.is_joy_dualsense(current_device): # pseudocode function for demo purposes
    Input.joy_adaptive_triggers_feedback(...)

Ideally, the adaptive trigger functionality should play for anyone who has it, but shouldn't affect those who don't. If the functions themselves check for adaptive trigger support and early-return if it's not there, then the game developer's code won't need to check.


I don't know if this should be a blocker for these functions themselves being merged, but I do think we should have more descriptive explanations of each of the adaptive trigger functions. It looks like the gist you linked with the factory functions describes the effects; Apple's GCDualSenseAdaptiveTrigger documentation also describes how they feel and work. We certainly want to make sure the Godot documentation is accurate to how the functions actually work in Godot (not just copying whatever these other sources say, but testing it for ourselves in Godot) but these may be good starting points.

@Nintorch
Copy link
Copy Markdown
Member Author

Should there be a way to setup trigger effect for both triggers in one method call, for example, by passing JOY_AXIS_TRIGGER_LEFT | JOY_AXIS_TRIGGER_RIGHT as the trigger parameter?

@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Oct 17, 2025

That would make sense to me! Unfortunately, it looks like JOY_AXIS_TRIGGER_LEFT = 4 (0b100) and JOY_AXIS_TRIGGER_RIGHT = 5 (0b101), so JOY_AXIS_TRIGGER_LEFT | JOY_AXIS_TRIGGER_RIGHT = JOY_AXIS_TRIGGER_RIGHT, and you wouldn't be able to distinguish both triggers from just the right one 🙁 If there's a different way it could be done though I think it'd be great.

@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Oct 18, 2025

The test project is now up on GitHub at https://github.com/Meorge/godot-test-adaptive-triggers !! 🥳 It has scenes to test all of the DualSense adaptive trigger methods; Input.send_joy_packet() is not explicitly supported, although it's used internally for all of the methods, so it's covered anyways.

Due to differences in how I thought the methods worked versus what they actually do after testing, some of the UI layouts might not totally make sense, but they still work well enough to see the adaptive triggers in action. I may be able to study the existing docs on the adaptive triggers, as well as do my own experimentation and testing, to write more descriptive documentation for how the effects feel over the next couple of days.

@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Oct 19, 2025

Here are the notes I collected on how the adaptive trigger functions work, along with some graphs I generated as visual aids 😄

While I hope everything here is accurate, if anyone else can take a look at it and double-check things for accuracy, that would be great – it wouldn't surprise me if I missed something here!

It's late for me now, but tomorrow I can try to submit suggestions in a review for refining the PR's documentation to fit these descriptions better.


Feedback

position is an integer in the range [0, 9] with 0 being the least depressed and 9 being the most depressed.

  • 0 - 0% depressed
  • 1 - 10% depressed
  • 2 - 20% depressed
  • 3 - 30% depressed
  • ...
  • 9 - 90% depressed

strength is an integer in the range [0, 8] with 0 offering no resistance and 8 offering the most resistance.

  • 0 - 0% resistance
  • 1 - 12.5% resistance
  • 2 - 25% resistance
  • 3 - 37.5% resistance
  • 4 - 50% resistance
  • 5 - 62.5% resistance
  • 6 - 75% resistance
  • 7 - 87.5% resistance
  • 8 - 100% resistance

joy_adaptive_triggers_feedback

The trigger will offer no resistance up until depressed to position, at which point depressing it the remaining distance will offer the resistance provided by strength.

Example: This GDScript code:

Input.joy_adaptive_triggers_feedback(0, JOY_AXIS_TRIGGER_LEFT, 6, 7)

would give the following resistance curve:
joy_adaptive_triggers_feedback

Apple description:

Sets the mode to provide feedback when the user depresses the trigger at the start position or at a greater value.

joy_adaptive_triggers_multi_feedback

Each index of strengths corresponds to a position.

The trigger will offer the resistance provided by strengths[position] while depressed within the range [position, position + 1].

Unlike joy_adaptive_triggers_feedback this method can allow multiple resistances to be felt depending on where the trigger is depressed.

Example: This GDScript code:

Input.joy_adaptive_triggers_multi_feedback(
    0,
    JOY_AXIS_TRIGGER_LEFT,
    [0, 0, 0, 4, 4, 4, 4, 2, 2, 2]
)

would give the following resistance curve:
joy_adaptive_triggers_multi_feedback

Apple description:

Sets the mode to provide feedback with the specified strengths for each possible trigger position.

joy_adaptive_triggers_slope_feedback

The trigger will offer no resistance up until it is depressed the amount provided by start_position. The resistance will then be start_strength and increase to end_strength as the trigger is depressed to end_position. Past end_position, the trigger will offer the resistance given by end_strength.

Example: This GDScript code:

Input.joy_adaptive_triggers_slope_feedback(0, JOY_AXIS_TRIGGER_LEFT, 3, 7, 2, 7)

would give the following resistance curve:
joy_adaptive_triggers_slope_feedback

Apple description:

Sets the mode to provide feedback when the user tilts the trigger between the start and the end positions.

Vibration

amplitude is an integer in the range [0, 8] with 0 having no vibration and 8 having the most vibration.

frequency is an integer in the range [0, 255] with 0 having no vibration and 255 having the highest-frequency vibration. With a low frequency (like 20), the trigger feels like an engine motor or automatically-firing gun. With a high frequency value (like 200), the trigger feels more like a constant vibration.

joy_adaptive_triggers_vibration

The trigger will not vibrate up until depressed to position, past which point it will vibrate with the given amplitude and frequency.

Example: This GDScript code:

Input.joy_adaptive_triggers_vibration(0, JOY_AXIS_TRIGGER_LEFT, 4, 7, 120)

would give the following amplitude curve:
joy_adaptive_triggers_vibration

Apple description:

Sets the mode to vibrate when the user depresses the trigger at the start position or at a greater value.

joy_adaptive_triggers_multi_vibration

The trigger will vibrate with the given frequency and the amplitude corresponding to amplitudes[position] while depressed within the range [position, position + 1].

Unlike joy_adaptive_triggers_vibration this method can allow the amplitude of the vibration to vary depending on how much the trigger is depressed. Note that the frequency of the vibration cannot be changed depending on the trigger's depression amount; there must be a single frequency at a time.

Example: This GDScript code:

Input.joy_adaptive_triggers_multi_vibration(
    0,
    JOY_AXIS_TRIGGER_LEFT,
    83,
    [0, 0, 3, 3, 3, 5, 5, 5, 5, 5]
)

would give the following amplitude curve:
joy_adaptive_triggers_multi_vibration

Apple description:

Sets the mode to vibrate with the specified amplitudes for each possible trigger position.

Weapon

joy_adaptive_triggers_weapon

The trigger will offer no resistance up until depressed to start_position. When depressed to within start_position and end_position, it will offer the resistance provided by strength. Once depressed farther than end_position, it will offer no resistance.

Example: This GDScript code:

Input.joy_adaptive_triggers_weapon(0, JOY_AXIS_TRIGGER_LEFT, 3, 7, 8)

would give the following resistance curve:
joy_adaptive_triggers_weapon

Apple description:

When the user depresses the trigger beyond the value of the end position, it stops providing feedback, giving the user a sense of release, similar to pulling a weapon trigger.

@Nintorch Nintorch force-pushed the dualsense-adaptive-triggers branch 2 times, most recently from 11b47d1 to 8a45074 Compare October 19, 2025 15:22
@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Oct 20, 2025

Looking at Apple's APIs for these controller functions, I see that they used normalized floats instead of integers for the arguments, and I think that could be a good idea to implement for Godot's API too. As I was building the testing project, I found myself often having to go back to the documentation to check what the valid ranges for each different parameter type were.

Having the API take in a normalized float and map it to the corresponding integer internally would make it more intuitive for the programmer, I think. There is the disadvantage that users may put in slightly different float values and expect different behavior, but instead get the exact same behavior (since the two float values map to the same integer). My gut says these differences won't be significant enough to notice, though.

Comment thread core/input/input.cpp Outdated
Copy link
Copy Markdown
Contributor

@Meorge Meorge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've suggested short descriptions of the adaptive trigger effects for the documentation 🙂

Some of the suggestions for joy_adaptive_triggers_feedback are also applicable to the other methods' documentation, but I held off on submitting those as separate suggestions until people could discuss and agree on them.

Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
@Nintorch Nintorch force-pushed the dualsense-adaptive-triggers branch from 8a45074 to ed3ceee Compare November 1, 2025 13:00
@Nintorch
Copy link
Copy Markdown
Member Author

Nintorch commented Nov 1, 2025

Thank you very much for the documenation improvements, Meorge! I included your suggestions into the code :)

@Nintorch
Copy link
Copy Markdown
Member Author

Nintorch commented Nov 1, 2025

Linux editor CI fails with erros in documentation about the order of parameters in joy_adaptive_triggers_vibration, but I'm not sure why it happens since its using the correct order, the one specified in the .h and .cpp files

@Nintorch
Copy link
Copy Markdown
Member Author

Nintorch commented Nov 1, 2025

It's because you use the wrong order in _bind_methods, that's where it gets the parameters from

I see! Thank you :)

@Nintorch Nintorch force-pushed the dualsense-adaptive-triggers branch from 303dad4 to 47d2032 Compare November 12, 2025 11:14
Comment thread core/input/input.cpp Outdated
Comment thread core/input/input.h Outdated
Comment thread core/input/input.h Outdated
Comment thread core/input/input.cpp Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread core/input/input.cpp Outdated
Comment thread core/input/input.cpp Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread core/input/input.cpp Outdated
Comment thread core/input/input.cpp Outdated
@Nintorch
Copy link
Copy Markdown
Member Author

Thank you for your review, AThousandShips! I included your suggested changes into the code! I hope I didn't forget anything :D

@Nintorch Nintorch force-pushed the dualsense-adaptive-triggers branch from 3368a35 to 1f173e6 Compare November 17, 2025 17:20
Copy link
Copy Markdown
Member

@AThousandShips AThousandShips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The style and documentation looks great save for a few final notes

Comment thread core/input/input.cpp Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
Comment thread doc/classes/Input.xml Outdated
@Nintorch Nintorch force-pushed the dualsense-adaptive-triggers branch from 1f173e6 to 88b602c Compare November 18, 2025 13:38
@Nintorch
Copy link
Copy Markdown
Member Author

Would be nice if we had HID input reading so we could determine if the triggers are actually affected.

I'm still not sure if that's necessary, sorry. Will it be used often? Would it be better than storing the triggers' state in a separate variable in the game code?
Can it be done in a separate PR?

@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Nov 19, 2025

For the adaptive triggers specifically, I think it would be best for the developer to not assume with their code that a player has adaptive triggers or that they work correctly. If they're there, then they can enhance the experience, but generally a game shouldn't be unplayable if the triggers aren't adaptive. So IMO what is in the PR right now is sufficient.

Another use case I can think of for HID input reading is determining the colors on Switch Joy-Con controllers. You have to send a query to the controllers asking for a particular range of their flash memory that contains the color data. This can then be used to display elements on-screen that are color-coordinated to the controllers players are holding in their hands, which is nice.

Perhaps this is a topic for another thread, but looking at the SDL APIs I'm not entirely sure how reading would work. I see this PR uses SDL_SendJoystickEffect(), listed in CategoryJoystick, but I don't see a corresponding "read" function. There are the HID functions in SDL, but there's no constructor from a joystick ID or SDL_Joystick struct that I've been able to pinpoint.

@Nintorch
Copy link
Copy Markdown
Member Author

Perhaps this is a topic for another thread, but looking at the SDL APIs I'm not entirely sure how reading would work. I see this PR uses SDL_SendJoystickEffect(), listed in CategoryJoystick, but I don't see a corresponding "read" function. There are the HID functions in SDL, but there's no constructor from a joystick ID or SDL_Joystick struct that I've been able to pinpoint.

I haven't used SDL HID functions, but I hope it's possible to open it via something like SDL_hid_open(SDL_GetJoystickVendor(joy), SDL_GetJoystickProduct(joy), NULL), https://wiki.libsdl.org/SDL3/SDL_hid_open

Comment thread core/input/input.cpp Outdated
Comment thread core/input/input.h
@Nintorch
Copy link
Copy Markdown
Member Author

Nintorch commented Jan 5, 2026

As it was briefly discussed on RocketChat, should we only expose Input.send_joy_packet(), while the adaptive triggers can be moved to a separate addon that uses Input.send_joy_packet()?

@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Jan 8, 2026

I'm feeling quite conflicted on this, and would love for someone to propose arguments in favor of keeping it in core, because on a personal level I do love having things built in and not needing to worry about addons.

But, based on what I know of the general Godot philosophy, at the moment it makes sense to me that the adaptive trigger functions would be moved to an addon, and only send_joy_packet() would be in core.

  • As far as I know, the DualSense controllers are the only ones with adaptive triggers, and the data format for them isn't standardized the same way that other controller features are. Thus it's likely to remain a PlayStation controller-specific feature for the foreseeable future. Having it exposed as via "generic" method (i.e., not directly linked to PlayStation controller functionality) in the Input API could suggest to users that it's more widespread than it actually is.
  • Adding support for a given feature also implicitly adds a maintenance burden, since we have to make sure that it continues to work in future versions of Godot and potentially with future controllers that support adaptive triggers. Other controller features like the RGB LEDs come free with SDL to my knowledge, so the maintenance is offloaded to SDL. That isn't the case with adaptive triggers.
  • There was the concern before about how the reverse-engineered packet structure for adaptive triggers worked with Godot's licensing. While we came to the conclusion that it was resolved, moving this into an addon would (I think) more fully protect main-Godot in case anything did come up with it.

I think in my ideal scenario, Godot would have classes of some kind to represent different controller types with specific input methods; for example, a DualSenseController class could have adaptive trigger methods, and a JoyConController class could have IR camera methods. Additional controller classes could be created as addons to address other specific controller features. But, that's a whole other can of worms, and best as a separate proposal 😅

Anyways, as I said before, on a personal level I would love to have these controller-specific features easily accessible from core. So if people have other arguments to make in favor of keeping them in core, I would very much appreciate them!

@Nintorch
Copy link
Copy Markdown
Member Author

Nintorch commented Jan 9, 2026

I will also have to check if we can even skip adding Input.send_joy_packet() and instead expose SDL's HIDAPI capabilities (there's a proposal for that too), maybe we can even make a constructor for HID device from a joypad ourselves! :D

@Meorge
Copy link
Copy Markdown
Contributor

Meorge commented Jan 9, 2026

In the longer run, I would love to have that. Using HID read/write commands, it's possible to interface with the Switch Joy-Cons and do things like get the controllers' physical colors and read data from the IR camera. Being able to play with these capabilities without needing to use GDExtension and hidapi would be quite nice 😄

Comment thread core/input/input.cpp
ClassDB::bind_method(D_METHOD("joy_adaptive_triggers_multi_feedback", "device", "axis", "strengths"), &Input::joy_adaptive_triggers_multi_feedback);
ClassDB::bind_method(D_METHOD("joy_adaptive_triggers_slope_feedback", "device", "axis", "start_position", "end_position", "start_strength", "end_strength"), &Input::joy_adaptive_triggers_slope_feedback);
ClassDB::bind_method(D_METHOD("joy_adaptive_triggers_multi_vibration", "device", "axis", "frequency", "amplitudes"), &Input::joy_adaptive_triggers_multi_vibration);
ClassDB::bind_method(D_METHOD("send_joy_packet", "device", "packet"), &Input::send_joy_packet);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might need to make this function not exposed at the moment, because it might be superseded by exposing HIDAPI later.

youfch added a commit to youfch/godot that referenced this pull request Mar 26, 2026
@Nintorch Nintorch marked this pull request as draft May 1, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants