Skip to content

hid: tmff2: add Thrustmaster T500RS wheel base driver#186

Open
cazzoo wants to merge 15 commits intoKimplul:masterfrom
cazzoo:hardening/t500rs-driver
Open

hid: tmff2: add Thrustmaster T500RS wheel base driver#186
cazzoo wants to merge 15 commits intoKimplul:masterfrom
cazzoo:hardening/t500rs-driver

Conversation

@cazzoo
Copy link

@cazzoo cazzoo commented Dec 9, 2025

This driver has been built from scratch based the previous dirty driver (see #175) and on captures from ffbsdl tool ran in windows for all possible effects (through SDL2 library).

@cazzoo cazzoo force-pushed the hardening/t500rs-driver branch from 2ae18a3 to bd43bb9 Compare December 9, 2025 00:21
@cazzoo cazzoo marked this pull request as ready for review December 9, 2025 00:23
@cazzoo
Copy link
Author

cazzoo commented Dec 9, 2025

Hi @Kimplul ,

Here is a new version of the driver, fully rebuilt from scratch. I've been trying to do my best to get it done the right way, steering its content the best I could (with my knowledge). I'll discard the previous PR that is not optimal. This one should be supporting more effects, and should be better designed. I've been also working on documentation to make it more comprehensive and based on SDL2 real examples.

Again, I am fully open to your feedback (and also @MmAaXx500 ones) that were really valuable by the other PR, so when you will have time, I'd really happy to get to your review.

Thanks

Copy link
Owner

@Kimplul Kimplul left a comment

Choose a reason for hiding this comment

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

Thanks for continuing on this project, I was getting worried that we scared you off :)

I did a quick overview. The documentation in particular looks a lot better, having clear examples of what each packet does and how they build up to a full effect upload helped greatly. I haven't done any cross-referencing between the docs and implementation, as I still found quite a lot of smaller issues I'd like taken care of first. I'll do a more thorough review after that.

Not entirely sure what you mean by 'from scratch', for instance the comment explaining the set_gain rationale is identical between this and the previous PR.

@cazzoo
Copy link
Author

cazzoo commented Dec 11, 2025

Got the code and the PR updated based on all your feedback. All were valid, the only ones I haven't worked on are the SQUARE effect I plan to work on later and the git version (that I can easily remove).
Let me know any other findings or your thoughts on this updated code

@cazzoo cazzoo force-pushed the hardening/t500rs-driver branch 2 times, most recently from 9e6319b to b7d188d Compare December 12, 2025 23:01
@cazzoo
Copy link
Author

cazzoo commented Dec 12, 2025

Hi @Kimplul ,

I've been working again on the PR, responding to all your points (almost all -- if not all -- were relevant), and introducing some missing features (FF_SQUARE missing and 0x05 conditional effect that was incorrectly done). If you mind having another pass, I'd be really happy.
Thanks

@Kimplul
Copy link
Owner

Kimplul commented Dec 17, 2025

Sorry about the delay, bit busy before the end of the year. I had a look through the resolved comments, most looked good but there were a few that I decided to unresolve, please have a second look at them.

I still haven't cross-referenced documentation to the code, I'll try and get that done by the end of next week, but based on a cursory look the code looks a lot better now. Seems you managed to shave off ~400 lines of code, well done.

Please also add a copyright statement to the start of the files you created, something like https://elixir.bootlin.com/linux/v6.18.1/source/drivers/hid/hid-retrode.c for example. I really should do that as well to the bits I wrote, but this is the first 'major' new addition to the driver so I've never really had any reason to think about it before.

@cazzoo
Copy link
Author

cazzoo commented Dec 17, 2025

No worries, thank you for the feedback you provided. Take the necessary time for whatever need review. I am not in the hurry, and this driver does work for me already so It can last as long as necessary from your end.

I'm happy we trend to get somewhat a stable mergeable version.
I just pushed a new part of code that handled previously the mode switch (from boot/init to normal mode). This is the part where I have the least confidence. The logic is fully fine, it's working well, but I'm not sure at all on code quality and the compliance/integration with the base driver. If you may give a glance when you can, I'd be happy to send some more time if needed.

Cheers

@cazzoo cazzoo force-pushed the hardening/t500rs-driver branch from 408f5bf to 1c5af2c Compare December 17, 2025 23:02
@Kimplul
Copy link
Owner

Kimplul commented Dec 18, 2025

I just pushed a new part of code that handled previously the mode switch (from boot/init to normal mode). This is the part where I have the least confidence.

That's the responsibility of hid-tminit, is it not working? Regardless, please revert and if needed, open up a separate PR in https://github.com/Kimplul/hid-tminit/tree/tspc.

@cazzoo
Copy link
Author

cazzoo commented Dec 18, 2025

I just pushed a new part of code that handled previously the mode switch (from boot/init to normal mode). This is the part where I have the least confidence.

That's the responsibility of hid-tminit, is it not working? Regardless, please revert and if needed, open up a separate PR in https://github.com/Kimplul/hid-tminit/tree/tspc.

It is indeed not working with the current code. I'll revert this. Not sure how I should update the init driver yet. Maybe just specifying the boot mode, will test.
Otherwise, if you have any idea based on my commit, let me know


EDIT: I just open a PR against hid-tminit for TSCP branch (even though it's t500rs code update): Kimplul/hid-tminit#2

I tested and it appears to work fine with that code and my driver (and the mode switch reverted from within the driver)

@Kimplul
Copy link
Owner

Kimplul commented Dec 18, 2025

EDIT: I just open a PR against hid-tminit for TSCP branch (even though it's t500rs code update)

Yeah, I intended to merge tspc into usb as part of adding support for the TS-PC, but seems I forgot. I'll sort it out once we deal with this PR, don't want to start changing too many moving bits at the same time.

@cazzoo
Copy link
Author

cazzoo commented Dec 18, 2025

There we are @Kimplul , I've been addressing all the points I guess, and introduced some fix also here or there (you can check latest commits, small scoped). Take your time for the documentation cross-check and let me know, in parallel I'm trying to improve the bits where I can

Copy link
Owner

@Kimplul Kimplul left a comment

Choose a reason for hiding this comment

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

Sorry about the delay, I've been busier than expected. I mainly focused on the docs and found some inaccuracies and points of improvement, please have a look at those.

I applied some style fixes myself, please cherry-pick them from https://github.com/Kimplul/hid-tmff2/tree/cazzoo-hardening/t500rs-driver. I don't have push access to your branch and opening a PR for a PR seemed a bit silly, but the changes were trivial enough that I didn't really want to request them explicitly. Kind of a clumsy part of the PR flow, but eh.

Regarding style, I've tried to follow the Linux Kernel style guide in this repo. Most significantly, it dictates a tab width of 8 spaces, whereas this has a width of 2, please fix that. You might also want to try running your code through clang-format with the kernel's style configuration, see https://www.kernel.org/doc/html/next/dev-tools/clang-format.html

@cazzoo cazzoo force-pushed the hardening/t500rs-driver branch from 1bf936c to 5c31c0e Compare January 16, 2026 13:41
@cazzoo
Copy link
Author

cazzoo commented Jan 16, 2026

Thank you @Kimplul for all the feedback. No worries for the delay, I also had a lot of parallel work to take care of in the meantime.
Your feedback were valuable, as usual, and it helped to find some odd things (fixed Saturation for instance) that I fixed I believe, making the driver more stable and robust.

Now I believe the code and much more ready, and the documentation updated appropriately and more accurately. Please have another pass and I'd be happy to change anything that's not fine by your POV.

@cazzoo cazzoo force-pushed the hardening/t500rs-driver branch 2 times, most recently from 5015d21 to 3e8c027 Compare January 16, 2026 14:12
Copy link
Owner

@Kimplul Kimplul left a comment

Choose a reason for hiding this comment

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

At least the documentation still needs a bit of work. I would suggest maybe to read through everything a couple of times from a perspective of someone who doesn't know anything about the wheel, reading from top to bottom to get a good overall picture of what the wheel does and how. Present things in the order that they're needed, lots of examples, try to maintain a consistent way to present information, stuff like that.

41 00 41 01 - START command
```
- **Packet Type:** 0x41 (Command)
- **Effect ID:** 0x00 (always 0x00 for T500RS)
Copy link
Owner

Choose a reason for hiding this comment

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

I believe we've already established that the effect ID varies depending on what effects are loaded, no?

41 00 00 01 - STOP command
```
- **Packet Type:** 0x41 (Command)
- **Effect ID:** 0x00 (always 0x00 for T500RS)
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.


**Important Notes:**
- **Envelope Support:** Envelope packets (0x02) have limited support. Non-zero envelope values cause EPROTO errors on periodic and constant effects. Always send zeros for envelope parameters on these effect types.
- **Runtime Updates:** Effect updates (via `update_effect` callback) only modify parameter-specific packets (0x03, 0x04, 0x05). Duration and delay changes require re-uploading the entire effect.
Copy link
Owner

Choose a reason for hiding this comment

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

Please clarify that this is only a limitation of this driver, and not of the FFB subsystem in general. Just in case someone reference this documentation when trying to figure out how FFB works.

4 | 2 | duration_ms | Duration in milliseconds, little-endian
6 | 2 | delay_ms | Delay before start, little-endian
8 | 1 | reserved1 | 0x00
9 | 2 | packet_code_1 | Code for subsequent packet type (variable!)
Copy link
Owner

Choose a reason for hiding this comment

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

packet_code_1 and packet_code_2 are the same as param_sub and envelope_sub, are they not? I think param_sub and envelope_sub are more descriptive terms, the code uses them as well.

| 0x40 | Spring | Windows driver captures |
| 0x41 | Damper/Friction/Inertia | Windows driver + FFEdit captures |

**Note:** Square wave (0x20) was discovered in FFEdit captures. The Windows driver may not expose this effect type through the standard API.
Copy link
Owner

Choose a reason for hiding this comment

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

The windows driver most definitely exposes the square wave. FFEdit uses the Windows API, so I'm not entirely sure what this is referencing anyway?


**Parameter Details:**
- Magnitude: 0-127 (scaled from Linux 0-32767)
- Offset: s8 (-128 to +127, DC bias (Direct Current bias - a constant force offset), scaled from Linux -32768 to +32767)
Copy link
Owner

Choose a reason for hiding this comment

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

Direct Current bias - a constant force offset I mean I know what you're trying to say, but you're still not saying very well and I'm not sure a first-time reader would really get what you're trying to say.

A graphical example would probably be pretty good here, maybe you could try for some ASCII art? At the very least, I would like to see it explained out in full, something like A constant offset means that the periodic effect is stronger when twisting the wheel to one side and weaker to the other. Feel free to come with your own suggestions, I'm not much of a wordsmith :)

- Magnitude: 0-127 (scaled from Linux 0-32767)
- Offset: s8 (-128 to +127, DC bias (Direct Current bias - a constant force offset), scaled from Linux -32768 to +32767)
- Phase: 0-255 (256 steps for 360 degrees, scaled from Linux 0-35999)
- Period: Direct milliseconds (no Hz conversion!)
Copy link
Owner

Choose a reason for hiding this comment

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

What is this Hz conversion?

- effect_type = 0x21 (triangle wave)
- Same envelope and periodic packet structure as sine wave

**Note:** Waveform type determined by effect_type in main packet, not in periodic packet parameters.
Copy link
Owner

Choose a reason for hiding this comment

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

Would it be correct to say that each waveform type is its own effect_type?

| 6 | 0x00b6 | 0x00c4 |

### Driver Implementation Notes
- **Effect ID Handling:** The driver uses hardware IDs 1-15 to avoid quirky behavior with hardware index 0 (only valid for constant effects).
Copy link
Owner

Choose a reason for hiding this comment

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

This is already mentioned above. Sometimes repetition can be useful, but there's only like 15 lines between each occurence so one or the other should probably be removed.

- **Device Format:** 16-bit little-endian (0-35999 in 0.01 degree units)
- **Conversion:** `device_dir = (os_ffb_dir * 36000) / 65536`
- **Examples:**
- 0degrees = 0x0000
Copy link
Owner

Choose a reason for hiding this comment

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

I believe it's customary to put a space between the number and degrees, presumably because degree is a whole word. Short forms, such as ms for millisecond, can be written without a space between.

@SeitzB
Copy link

SeitzB commented Jan 20, 2026

I have been following this development with some excitement and just tried to build and run the current PR. I do not know how to properly mention files in a comment, but in the hid-tmt500rs-usb.c file there is a capital i after a semicolon.

@cazzoo
Copy link
Author

cazzoo commented Jan 20, 2026

Good catch! Thank you very much for the report

@Kimplul
Copy link
Owner

Kimplul commented Jan 20, 2026

@SeitzB I think you have the wrong branch checked out, master seems to have the spurious I while the most recent changes are in hardening/t500rs-driver.

Copy link
Owner

@Kimplul Kimplul left a comment

Choose a reason for hiding this comment

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

I added some comments about the most recent stylistic changes. Looks like clang-format did something weird with the whitespacing in multi-line comments, or maybe they were weird to begin with and clang-format just choked. Dunno.

u8 phase, u16 period_ms)
{
/* Byte order per Windows USB captures (example: 04 2a 00 06 00 3f 0a 00):
* b0=T500RS_PKT_PERIODIC, b1=code, b2=reserved1, b3=mag, b4=offset,
Copy link
Owner

Choose a reason for hiding this comment

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

Quick nitpick: Something funky with the comments, seems to use mixed spaces and tabs?

/* Wheel handles positive magnitudes only */
projected = -projected;

/* Add 180 degrees to phase to maintain correct force direction.
Copy link
Owner

Choose a reason for hiding this comment

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

Same here

offset = (s8)((end_level - start_level) / 512);

/*
* Phase encodes ramp direction per FFEdit captures:
Copy link
Owner

Choose a reason for hiding this comment

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

Same

*/
phase = (start_level < end_level) ? 0x7f : 0x00;

/* Byte order per USB captures: b0=id, b1=code, b2=reserved1, b3=mag,
Copy link
Owner

Choose a reason for hiding this comment

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

Same

p->id = 0x02;
p->subtype = subtype;

/*
Copy link
Owner

Choose a reason for hiding this comment

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

Same


T500RS_DBG(t500rs, "Sending initialization sequence...\n");

/* Report 0x42 - Init/status commands (2 bytes each)
Copy link
Owner

Choose a reason for hiding this comment

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

More comment whitespace issue

hid_warn(t500rs->hdev, "Init command 0x42 0x00 failed: %d\n",
ret);

/* Report 0x40 - Enable FFB (4 bytes)
Copy link
Owner

Choose a reason for hiding this comment

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

Comment whitespace issue

hid_warn(t500rs->hdev,
"Init command 3 (0x40 config) failed: %d\n", ret);

/* Report 0x43 - Set global gain (2 bytes)
Copy link
Owner

Choose a reason for hiding this comment

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

Same

u8 right_sat = (cond->right_saturation * 100) / 65535;
u8 left_sat = (cond->left_saturation * 100) / 65535;

struct t500rs_pkt_r05_condition *p =
Copy link
Owner

Choose a reason for hiding this comment

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

Not required to get merged, but the high level of indent here makes the code a bit annoying to read, some kind of refactoring could be useful here, such as instead of t500rs_build_r05_condition and t500rs_send_hid you could instead just have t500rs_send_condition.

break;
}

case T500RS_SEQ_CONDITION_Y: {
Copy link
Owner

Choose a reason for hiding this comment

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

Does anyone use the second axis? The T300 only uses effect->u.condition[0], and I don't remember seeing kernel drivers use condition[1].

@Kimplul
Copy link
Owner

Kimplul commented Feb 2, 2026

Commit c6d27c3 says:

Addresses all 38 unresolved review comments from Kimplul's Jan 2026 review

That's not true. Most of the issues I pointed out still remain. Did you push the commit by accident or what's the deal here?

@cazzoo
Copy link
Author

cazzoo commented Feb 3, 2026

That was indeed not intended to be pushed straight away, I'm still having a few time to work on it and probably pressed the wrong button. Sorry for the inconvenience

@cazzoo cazzoo force-pushed the hardening/t500rs-driver branch from ab8801b to 8d749d8 Compare February 25, 2026 15:10
@cazzoo
Copy link
Author

cazzoo commented Feb 25, 2026

@hoover67 I got the build fixed (and took the opportunity to rebase the code branch onto Kim's master branch).
Please give it another try. Commit ID is now 8d749d8

@hoover67
Copy link

thanks @cazzoo!

I need a bit of help pulling the correct version. I'll clone the repo as usual and then.. how do I switch to the commit ID you mention above?

Would

git checkout <commit_hash>

do the trick, and then build the module as usual from there in my current src/hid-tmff2 directory that already exists?

Sorry I'm a complete git noob (well except for the standard git clone stuff, that is :-))

thanks, Uwe

@hoover67
Copy link

Nevermind, I think I managed to get the current version with some help from gpt... the module builds now, I hope I didn't do anything wrong: (bash history)

2001  git clone --recurse-submodules https://github.com/Kimplul/hid-tmff2.git
 2002  cd hid-tmff2
 2003  git remote add cazzoo https://github.com/cazzoo/hid-tmff2.git
 2004  git fetch cazzoo
 2005  ls 
 2006  sudo ./dkms/dkms-install.sh 
 2007  sudo make udev-rules

@hoover67
Copy link

hoover67 commented Feb 25, 2026

ok here's what happens after building & installing the module & udev-rules, unplugging the wheel and rebooting.

plug in the wheel:

   44.357217] usb 3-1: Product: Thrustmaster FFB Wheel
[   44.357220] usb 3-1: Manufacturer: Thrustmaster
[   44.407857] input: Thrustmaster Thrustmaster FFB Wheel as /devices/pci0000:00/0000:00:01.2/0000:20:00.0/0000:21:08.0/0000:2a:00.3/usb3/3-1/3-1:1.0/0003:044F:B65D.000D/input/input26
[   44.407987] hid-thrustmaster 0003:044F:B65D.000D: input,hidraw9: USB HID v1.00 Gamepad [Thrustmaster Thrustmaster FFB Wheel] on usb-0000:2a:00.3-1/input0
[   44.428210] hid-thrustmaster 0003:044F:B65D.000D: URB to get model id failed with error -32
[   44.430472] Error: Driver 'hid-thrustmaster' is already registered, aborting...

sudo oversteer 

reports "no device available".

root@dirac:~# modprobe hid-tminit-new
modprobe: ERROR: could not insert 'hid_tminit_new': Device or resource busy
root@dirac:~# modprobe hid-tmff-new 

root@dirac:~# lsmod | grep ff
hid_tmff_new           61440  0
hid                   180224  6 usbhid,hid_thrustmaster,hid_winwing,hid_generic,hid_cherry,hid_tmff_new

sudo oversteer still reports "no device available".

After running "tmdrv" above, I can now run "sudo oversteer" but I can no longer change any settings like wheel range, ffb and so on (which worked with the old module version I was running before).

I'd be very happy to hear what I'm doing wrong (for one, there should be no need to modprobe the modules manually, right? Also I'm still confused why I still need to run tmdrv in order for oversteer to recognize the wheel.

I'll test if ffb still works in any of the titles where it worked before updating to the current build.

Thanks in advance & all the best,

Uwe

@hoover67
Copy link

Hi folks,

FFB effects no longer work in AM2 and AC Rally, both only display a constant centering force now.

rfactor2 also has the same constant centering force now which is an improvement of sorts because I could no longer get the ffb to work even once (as described) using the procedure outlined above with the old module version I was running. The wheel would go completely limp in rfactor2.

All the best,

Uwe

@hoover67
Copy link

Hey, probably my final report for tonight :-)

after reverting back to version 0.82 that I was running before, FFB effects are back in AC Rally, also I can set the wheel range again using "sudo oversteer".

When I ran the "tests" oversteer thingy with the most recent version of the module I got a python error (didn't catch it sadly) about a "missing effect wheel left" or similar in the console where I started sudo oversteer from, so it looks like the input event device doesn't advertise the FFB caps of the wheel.

This is no wonder because at no point did I see this message with the current version of the module:

[   63.521607] hid-generic 0003:044F:B65E.000E: input,hidraw9: USB HID v1.11 Joystick [Thrustmaster TRS Racing wheel] on usb-0000:2a:00.3-2/input0
[   63.592813] input: Thrustmaster TRS Racing wheel as /devices/pci0000:00/0000:00:01.2/0000:20:00.0/0000:21:08.0/0000:2a:00.3/usb3/3-2/3-2:1.0/0003:044F:B65E.000E/input/input28
[   63.644715] tmff2 0003:044F:B65E.000E: input,hidraw9: USB HID v1.11 Joystick [Thrustmaster TRS Racing wheel] on usb-0000:2a:00.3-2/input0
[   63.668126] tmff2 0003:044F:B65E.000E: T500RS initialized successfully (HID mode)
[   63.668130] tmff2 0003:044F:B65E.000E: FFB: set_gain 39999 -> device 155
[   63.672127] tmff2 0003:044F:B65E.000E: FFB: Gain set successfully

which I am only seeing with the older version 0.82.

If there's any other info I can provide please let me know & thanks for your hard work!

All the best,

Uwe

@cazzoo
Copy link
Author

cazzoo commented Feb 26, 2026

Hi @hoover67 , I think you are pulling the master branch from my fork repository, that may explain the loss of FFB.
Please run the following command (from your folder):
git checkout -b hardening/t500rs-driver cazzoo/hardening/t500rs-driver

Then, build & insert module, either using dmks script or make && sudo make install.

You shall get FFB back on AMS2, Dirt Rally2, ...etc.

In parallel, I've tried to get FFB in RFactor2 (that I just bought to test FFB) and got none. I haven't review the python script you shared, but that definitely sound a bit OTT to get FFB working. Please note that I encountered the same issue in RRRE (Raceroom) and haven't find any solution yet.
I'll check what's about your script and see if there's something the driver misses or can manage to get that FFB working in those games

@hoover67
Copy link

Hey @cazzoo,

thanks much for the update, I'll try it later tonight or over the weekend.

As for rfactor2, I got FFB working exactly once with version 0.82 using the method described above (unplug -> replug -> run tmdrv -> run oversteer as root -> start rfactor2), but following a reboot or other (sorry, I don't know exactly what happened in between sessions) I was not able to get it working in rfactor2 again.

Sadly I also don't recall if SteamInput was disabled or any of the HIDRAW runcommand magic was configured in Steam.

Could it be possible that other ffb sims accessing the wheel before starting rf2 could have "paved the way" for rfactor2 to pick up the wheel correctly?

AFAIK the tmdrv script only initializes the wheel & pedals so they can be used as a Joystick. I've used it mainly in past so I could assign the toe brakes and rudder axis to the T500RS pedals.

All the best,

Uwe

@cazzoo
Copy link
Author

cazzoo commented Feb 26, 2026

After numerous tests, I haven't found a solution for that issue that happens for both rfactor2 and RRRE. I believe that may be related to Wine/Proton and could be managed by a fix introduced here (but don't know what).
The python script you shared is just resetting the wheel, letting it re-enumerating, and doing mode switch (from boot mode to normal mode). This is handled by the hid-tminit driver, loaded before the hid-tmff driver (which loads then T500RS part). With the latest code, I believe that if you just unplug/replug the wheel, the behavior should be the same as the python script ran.

@Kimplul have you ever heard about that issue. When entering in-game (3D) first, FFB works, when getting to menu FFB drops, and then when getting back to in-game it doesn't work anymore.
The only solution to get FFB back is to re-enumerate the wheel for the game to start effects.

I noticed already during implementation that the games usually don't send start commands. So the wheel is playing once after enumeration, then the packets are being updated in-place (which work for most of the games, but RRRE and Rfactor2).
If the game would eventually stop the effect, there would be then no play effect triggered back, leading to loss of forces.

All of this is assumptions, but this is what I roughly feel

@Kimplul
Copy link
Owner

Kimplul commented Feb 26, 2026

@Kimplul have you ever heard about that issue.

Can't say I really have. You can try using ffbwrap from https://github.com/berarma/ffbtools to dump what effects the game is trying to send the driver. If the effects suddenly stop, it's likely something with Proton/SDL.

If the game keeps uploading effects but nothing is happening with the wheel, check that the driver is actually sending out packets, for example by just adding some print statement to t500rs_send_hid.

If the driver keeps sending packets, then the driver is doing something that the wheel doesn't like, but no clue what that could be.

@Kimplul
Copy link
Owner

Kimplul commented Feb 26, 2026

I noticed already during implementation that the games usually don't send start commands. So the wheel is playing once after enumeration, then the packets are being updated in-place (which work for most of the games, but RRRE and Rfactor2).
If the game would eventually stop the effect, there would be then no play effect triggered back, leading to loss of forces.

Oh, I suppose you already checked ffbwrap. Sorry, didn't read through your comment properly.

Updating effects in-place is explicitly allowed by the FFB API, so it maybe seems like there's something going wrong in t500rs_update_effect(). Are you sure that the effects stop when you do something specific in-game, and not just after a given time, like thirty seconds after starting the game or something along those lines? The effect duration handling received quite a lot of comments previously, I wouldn't necessarily be surprised if it's causing issues.

@hoover67
Copy link

Hi folks,

I used the git command above to follow the hardened branch in the hid-tmff2 directory, recompiled the modules (which worked) and installed them using sudo make install and install-udev.

I still have to use the "tmdrv" command or "oversteer" will complain about "no device found". Once I run tmdrv to init the wheel, the driver gets loaded according to "dmesg".

FFB is back in AC Rally and Automobilista 2. I had to re-assign the controls in AC Rally but a major patch was released today (0.3, it's still in early access) so that may have been the cause.

AM2 did not require any reassignments.

in rfactor2, I selected "disable SteamInput" and checked the Proton version I'm using (9.0) with this title.

I'm happy to report that this time, rfactor2 had FFB until I returned from 3d on-track to the 2d UI. Re-entering the track a 2nd time, FFB was gone again.

I have not rebooted the machine in between game launches, but I'll try that next (hopefully not losing my comment here for the umpteenth time :-)) and report back.

All the best, Uwe

@cazzoo It's very kind of you the shell out real money in order to test rfactor2. If you'd like to be re-imbursed for your troubles I'm happy to contribute :-)

@cazzoo
Copy link
Author

cazzoo commented Feb 26, 2026

I nailed the issue a bit down and found some more clues. So confirming the issue happens for both games, I found that the game (when doing alt+tab or get back to pits) is stopping effects. When entering back in race, somehow, the effects are not started back..
Here are some debug logs I captures while this issue happens:

## pressed alt+tab
[32861.719208] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[32861.719318] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[32861.719417] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[32861.719507] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[32861.719592] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[32861.719980] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[32861.720071] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[32861.720075] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[32861.720078] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[32861.720081] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[32861.720084] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[32861.720088] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[32861.720091] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[32861.720094] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[32861.720097] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[32861.720099] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[32861.720310] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOPPED (type=81)
[32861.723410] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOPPED (type=81)
## got back ingame (alt+tab), no FFB
[32861.723698] tmff2 0003:044F:B65E.0020: FFB: Effect 0 START requested (count=2147483647, type=81)
[32861.723930] tmff2 0003:044F:B65E.0020: FFB: Effect 1 START requested (count=2147483647, type=81)
[32861.724150] tmff2 0003:044F:B65E.0020: FFB: Effect 2 START requested (count=2147483647, type=85)
[32861.724427] tmff2 0003:044F:B65E.0020: FFB: Effect 3 START requested (count=2147483647, type=83)
[32861.739411] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STARTED (type=85, count=2147483647)
[32861.755421] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STARTED (type=83, count=2147483647)
[32861.771467] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOPPED (type=82)

So I tested adding an extra re-init script (manually triggered) : Stop work handler, Reset all effect states and then Reinitialize wheel (tmff->open), and that worked!

So I don't exactly know if there's a bug to fix in Wine/Proton, but that workaround worked. I'm trying to get it added to the base driver but I'm not entirely convinced this is the right place, especially knowing this is shared code with other wheel bases...

@Kimplul
Copy link
Owner

Kimplul commented Feb 26, 2026

So I tested adding an extra re-init script (manually triggered) : Stop work handler, Reset all effect states and then Reinitialize wheel (tmff->open), and that worked!

So I don't exactly know if there's a bug to fix in Wine/Proton, but that workaround worked. I'm trying to get it added to the base driver but I'm not entirely convinced this is the right place, especially knowing this is shared code with other wheel bases...

I don't think that's a good approach. From the logs it looks like effects 0-3 are all requesting a start, which the driver should be able to handle just fine. Not sure why the game seems to be sending multiple stop signals, but that shouldn't matter.

Looks like all the messages are printed within ~50 milliseconds. Is it maybe possible that the driver handles the STOP requests, and then ignores the START requests because they arrived 'too soon'?

@hoover67
Copy link

Thanks for putting in the work debugging this folks. rfactor2 supports plugins. Is this something that could be handled on a plugin level maybe?

rfactor2 even has an FFB reset function (I wonder why) but sadly this does nothing to fix the problem in this case (I've tried :-))

Sorry in advance if this is a stupid idea and / or won't work, but maybe a few lines in an rf2 / RR plugin is all that is needed...

All the best,

Uwe

@Kimplul
Copy link
Owner

Kimplul commented Feb 26, 2026

@hoover67 As far as I can tell, the game itself (+Proton+SDL etc.) is behaving as it should and is following the FFB API, but the driver is not responding correctly, therefore the driver should be fixed. I'm sure there are all kinds of workarounds that could be implemented, but I don't want to accept clearly incorrect code into this repository. I hope there's no rush here and we can take the time to do things properly :)

@SeitzB
Copy link

SeitzB commented Feb 26, 2026

Maybe it could be helpful if we looked at what a working driver does? I have a Logitech G27 which has relatively mature drivers afaik. I could buy rfactor and try it with the g27 and provide you with whatever logs might be helpful to you. And if it also doesn't work with the G27 we would have confirmation that it may not be a driver issue after all?

Just an offer

@cazzoo
Copy link
Author

cazzoo commented Feb 26, 2026

Maybe it could be helpful if we looked at what a working driver does? I have a Logitech G27 which has relatively mature drivers afaik. I could buy rfactor and try it with the g27 and provide you with whatever logs might be helpful to you. And if it also doesn't work with the G27 we would have confirmation that it may not be a driver issue after all?

Just an offer

That's an interesting offer, if you mind that could definitely help.
What would be interesting is the ffb start/stop events, I am not sure your driver reports but it would be nice to compare.

Rfactor2 is not overpriced, and you can get refund if you manage to capture information rather quick

@Kimplul
Copy link
Owner

Kimplul commented Feb 26, 2026

@SeitzB Appreciate the offer, can't hurt to compare against another implementation but at least my T300 works with RRRE and rFactor2. Or, at the very least has worked, haven't played either in a while but either way the games themselves are likely working as intended.

@cazzoo
Copy link
Author

cazzoo commented Feb 27, 2026

Just to let you know, I figured that if I restrict the driver to use only 1 effect slot, I don't have the issue.
I am currently investigating a concurrency/race condition issue when multiple effects are started/stopped, may lead to inconsistencies when starting effects back (probably attempting to set an effect already booked into a slot that is not properly stopped/completed).
I'll keep you in touch

@cazzoo
Copy link
Author

cazzoo commented Feb 27, 2026

@Kimplul, I managed to understand what was going wrong, and it's not particularly due to t500RS code, but due to the fact I am sending interrupts and not HID commands, more particularly it's how the base driver (tmff2) is handling multiple stop/start events while focusing out and in the game.

I tested multiple times and this completely fixed the issue.

The rationale is:
When a game sends START requests while the work handler is processing effects, a race condition can occur where the START flag is cleared in the first loop but the effect is not actually started until the second loop, causing the effect to never start.
The solution is to batch process the events in a 3 steps sequence.

Because this is impacting the base driver, I decided not to commit directly into my branch, but created dedicated branch and a PR from my branch towards my branch, with the fix included: cazzoo@83bbd95

Would you mind checking the code and telling me if you see anything anti-pattern or against the base code, that may break the other wheels.

@Kimplul
Copy link
Owner

Kimplul commented Feb 27, 2026

When a game sends START requests while the work handler is processing effects, a race condition can occur where the START flag is cleared in the first loop but the effect is not actually started until the second loop, causing the effect to never start.
The solution is to batch process the events in a 3 steps sequence.

That doesn't make sense to me. Setting and clearing the flags is done within a spinlock, where exactly would the race condition be?

If you mean that the worker sees a STOP, and while handling the STOP, a START arrives, I could see that the START is not picked up. However, since the worker is running, delayed_work_pending() should return false, and the work handler should run a second time, triggering the START. Maybe check if the work handler stops running after an alt-tab? That would be my preferred solution, you solution seems to imply that both REQUEST_START and REQUEST_STOP would be set, which should never be the case.

How is the interrupt/hid messaging stuff related? Is the interupt just slower, meaning a greater chance of mismatched start/stop?

@cazzoo
Copy link
Author

cazzoo commented Feb 27, 2026

That doesn't make sense to me. Setting and clearing the flags is done within a spinlock, where exactly would the race condition be?

If you mean that the worker sees a STOP, and while handling the STOP, a START arrives, I could see that the START is not picked up. However, since the worker is running, delayed_work_pending() should return false, and the work handler should run a second time, triggering the START. Maybe check if the work handler stops running after an alt-tab? That would be my preferred solution, you solution seems to imply that both REQUEST_START and REQUEST_STOP would be set, which should never be the case.

I don't really know why this happens, but as soon as we have multiple effects handled by the driver, it does fail to get all started effects to be stopped, and new effects started.
Here's a sample of my dmesg logs showing what's going when going focus-on after focus out:

[82662.259493] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82662.259515] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOPPED (type=81)
[82662.259609] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[82662.259891] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[82662.260047] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[82662.260142] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[82662.260482] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82662.260552] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82662.260555] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[82662.260557] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[82662.260559] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[82662.260561] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[82662.260564] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82662.260566] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[82662.260568] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[82662.260569] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[82662.260571] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[82662.261329] tmff2 0003:044F:B65E.0020: FFB: Effect 0 START requested (count=2147483647, type=81)
[82662.261625] tmff2 0003:044F:B65E.0020: FFB: Effect 1 START requested (count=2147483647, type=81)
[82662.262054] tmff2 0003:044F:B65E.0020: FFB: Effect 2 START requested (count=2147483647, type=85)
[82662.262393] tmff2 0003:044F:B65E.0020: FFB: Effect 3 START requested (count=2147483647, type=83)
[82662.286706] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STARTED (type=81, count=2147483647)
[82662.302683] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STARTED (type=85, count=2147483647)
[82662.318681] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STARTED (type=83, count=2147483647)
[82662.334683] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOPPED (type=82)
[82662.338687] tmff2 0003:044F:B65E.0020: FFB: set_gain 39999 -> device 155
[82662.342682] tmff2 0003:044F:B65E.0020: FFB: Gain set successfully
[82662.378859] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STARTED (type=81, count=2147483647)
FFB OK above - Just did alt-tab to focus out
[82668.263985] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82668.264008] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOPPED (type=81)
[82668.264081] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[82668.264150] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[82668.264228] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[82668.264303] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[82668.264732] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82668.264813] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82668.264818] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[82668.264820] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[82668.264822] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[82668.264824] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[82668.264827] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STOP requested (type=81)
[82668.264829] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOP requested (type=81)
[82668.264831] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STOP requested (type=85)
[82668.264833] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STOP requested (type=83)
[82668.264835] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOP requested (type=82)
[82668.266064] tmff2 0003:044F:B65E.0020: FFB: Effect 0 START requested (count=2147483647, type=81)
[82668.266665] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STOPPED (type=81)
[82668.267129] tmff2 0003:044F:B65E.0020: FFB: Effect 1 START requested (count=2147483647, type=81)
[82668.267960] tmff2 0003:044F:B65E.0020: FFB: Effect 2 START requested (count=2147483647, type=85)
[82668.268527] tmff2 0003:044F:B65E.0020: FFB: Effect 3 START requested (count=2147483647, type=83)
[82668.282681] tmff2 0003:044F:B65E.0020: FFB: Effect 2 STARTED (type=85, count=2147483647)
[82668.298665] tmff2 0003:044F:B65E.0020: FFB: Effect 3 STARTED (type=83, count=2147483647)
[82668.314692] tmff2 0003:044F:B65E.0020: FFB: Effect 4 STOPPED (type=82)
[82668.318634] tmff2 0003:044F:B65E.0020: FFB: set_gain 39999 -> device 155
[82668.322641] tmff2 0003:044F:B65E.0020: FFB: Gain set successfully
[82668.358634] tmff2 0003:044F:B65E.0020: FFB: Effect 0 STARTED (type=81, count=2147483647)
[82668.386675] tmff2 0003:044F:B65E.0020: FFB: Effect 1 STARTED (type=81, count=2147483647)
FFB KO above - Just did alt-tab to focus out

We can see the sequence is slightly different between focuses-in that works and the ones that doesn't work (the pattern is always the same when failing or succeeding). From my attempts, getting all the effects handled with the 3 phases sequence ensures the STOPS are all processed and then the STARTS can start.

I will definitely check the work handler, see if it stops or not when focus out.

How is the interrupt/hid messaging stuff related? Is the interupt just slower, meaning a greater chance of mismatched start/stop?

I don't really know, but the game send the exact same command in the same order to T500 and T300, and T300 handles it properly. T300 uses HID layer AFAIK whereas T500 uses USB interrupts, so my assumption is that HID layer is handling this.

@cazzoo
Copy link
Author

cazzoo commented Feb 27, 2026

So, I figured there's no issue with the work handler, it's still processing after game started, no matter an ALT+TAB has been pressed or whatsoever (getting back to game's pits menu).

After some more analysis, the T500RS hardware requires all effects to be uploaded before any are started. This is a device protocol requirement, not a software bug:

When doing Sequential, it failed: Upload effect0 → Start effect0 → Upload effect1 → Start effect1 → ...
When doing Three-pass, it worked: Upload all effects → Stop all effects → Start all effects

When we interleave uploads and starts, the device gets confused and FFB doesn't work. The three-pass approach ensures the device is in a consistent state before starting effects.

This corroborate with my previous finding: T500RS only upload and play once, then it's continuously sending updates.

I am happy to revisit the way the t500rs driver works, but I think we're getting to an edge-case that need to be dealt in a particular way, and I'm getting to the end of my knowledge to troubleshoot this case. @Kimplul, If you have any other suggestion, my ears are open and I'm fine to work back on the logic, just let me know.

From all my attempts, without that three-pass it was randomly failing (unless I forced only 1 effect slot for the driver), and at the moment I am putting in place that workaround, it's working all the time long (100% success rate).

@hoover67
Copy link

hoover67 commented Feb 28, 2026

EDIT: I tried the "git checkout" command mentioned above and I receive an error that the branch named already exists. Should I just delete everything and repeat the process?

Hey @cazzoo,

thanks for the update. Is there a way for me to test your "workaround" & maybe provide (hopefully helpful) feedback?

All the best,

Uwe

@cazzoo
Copy link
Author

cazzoo commented Feb 28, 2026

@hoover67 you have to do git fetch cazzoo origin (commands are not correct), then checkout branch "hardening/fix-multi-stop-start-race-condition" from cazzoo origin as well, and then build&install driver.
Recommended, every time you checkout a branch, to do git pull

@hoover67
Copy link

EDIT: Never mind, managed to (hopefully) checkout the correct version, will try it later on. Sorry for the noise! :-)

Uwe

@hoover67
Copy link

@cazzoo Looking great! Just tried rfactor2 with the latest version of your driver including the race condition workaround and I was able to enter the track several times without the wheel losing FFB effects... thanks so much guys for all your hard work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants