ValveSoftware / wine

Wine with a bit of extra spice
Other
1.27k stars 241 forks source link

Dead zone handling #94

Open kakra opened 4 years ago

kakra commented 4 years ago

Wine's current implementation of xinput uses SDL on Linux. This shows the following problem:

Microsoft clearly points out that xinput device have no dead zones on the API level, dead zones must be handled by the game itself: https://docs.microsoft.com/en-us/windows/win32/xinput/getting-started-with-xinput

But on Linux, SDL handles dead zones for evdev devices, and devices from joydev have dead zones at the kernel level. The current work-around is to set flatness to zero for such devices in the kernel. So Proton currently has no way to control this.

This has a big impact on any gamepad-controlled game played in Proton and makes a very bad experience when precision is needed such as in fast first person games, simulations etc. Evidence shows that the situation is greatly improved by setting the flatness to 0 at the Linux layer so SDL or joydev will no longer handle dead zones, in consequence Proton will see no dead zones from the lower layers and can pass unfiltered input data through the xinput API.

Since reading from evdev exposes no dead zones, the best approach would probably be to patch SDL to have an API call if one wants to receive input data without any dead zones applied. Proton could then just switch dead zones from SDL off and receive unfiltered inputs. Wine can already apply its own dead zone handler if games set that property, so that's probably all that's needed.

An intermediate solution would be that Proton makes that switchable on a game-per-game basis by setting flatness for devices to zero at launch time, and restoring the original value when the game quits. Maybe it's fine to just to that for any game and instead set the default Wine dead zone to 10%.

We currently implemented that in xpadneo by reducing the fuzz parameter by a factor of 8 for Xbox controllers which already makes a perceivable smoother and more direct input experience. We also shrunk the dead zone by 25% which improves the situation a little for games requiring precise control. We also added a driver option for high-precision mode which turns dead zones off, and that has a huge impact on game experience. xow is probably following soon at least partially with a similar solution. But this can only be a work-around as it messes with joydev (games and applications may not expect to have a joydev without dead zones). The xpad driver walks the middle ground by using 128 as the dead zone but this is no solution on either side because it makes joydev jittery without eliminating the dead zone for xinput games.

@aeikum I wonder what the best solution would be?

See also: https://github.com/atar-axis/xpadneo/pull/232 https://github.com/medusalix/xow/issues/105

aeikum commented 4 years ago

Can you give an example of a game that is impacted by this, and describe how I can reproduce the problem?

I agree getting some way to turn off the deadzone in SDL is probably the right solution.

kakra commented 4 years ago

So far I tried Borderlands 3 and AC:Odyssey myself: The differences are subtle but noticeable enough in general, and especially noticeable when trying to aim at a moving target during weapon zoom. Others reported Elite: Dangerous where it is supposed to make a huge difference while circling around targets, or X-Plane 11 which seems also depend a lot on correct dead zone implementation.

We should also think of this: Some games may change the sensitive curves on the fly, e.g. use sqrt() during zoom, which then would expand a pre-existing dead zone a lot and make precise aiming almost impossible. Such games would see a vastly improved situation. I'm not sure if E:D does something like this and uses a flattened sensitive curve in the inner zone of the sticks.

If you want to try yourself you can emulate this by setting the flatness value with evdev-joystick to 0 (to turn off dead zone handling in SDL) or a higher value than default 4095, e.g. around 7000 which Microsoft seems to recommend as the default dead zone. Games that use this default dead zone recommendation will probably see not much of a difference at all, as that dead zone is about twice as big as the default dead zone in Linux. Depending on the dead zone algorithm used, it either replaces the dead zone of Linux (for a naive implementation of jumping in and out of the dead zone), or expands it (for an implementation that rescales the remaining range outside of the dead zone, most games and drivers do this).

Drivers like xpad will almost see no difference at all because they already use a very tiny dead zone. So you may want to look at devices using drivers that use the default of Linux (which is around 12% for HID devices).

Since Microsoft documents xinput devices to have no built-in dead zone handling, we should expose the devices the same in Proton (which SDL currently does not let us do), no matter how big the perceivable difference is. Depending on each person and game, some may see an improvement, some may see no difference at all, some may feel like the input lag is gone. So it's a very subjective perception.

I recommend testing with a fast-paced FPS that has a weapon zoom mode or a simulation game, then either fiddle with evdev-joystick to set the dead zone (SDL will pick this value up during init). Or you switch between different drivers, like current xpadneo-master, xow, generic HID (built-in Xbox One S Controller support in hid-microsoft), or xpad.

kakra commented 4 years ago

BTW: Looking at the source code of SDL, SDL only handles dead zones in the Linux version of SDL. This is probably for replicating joydev behavior, as joydev also does this. And it looks like current SDL does not use joydev at all but only reads from evdev. Since evdev only recommends a dead zone but does not apply it to the values reported, SDL takes this job, so its consistent with joydev. But while native apps could use ioctls (probably) to turn off dead zones in joydev, SDL applications cannot. This would need a patch for Linux SDL to tell it per device that we want unfiltered values. So Proton's current option would be reading from evdev directly instead of going through SDL - which probably duplicates a lot of effort.

There's already a udev bus in winebus.sys which does this, but it doesn't seem to be made for exporting controllers to xinput... Maybe that would be an option: Improve that bus driver and discard SDL from Wine which also gets rid of a dependency used only for a single component.

aeikum commented 4 years ago

Thanks for the info. I still think fixing this in SDL is the right thing to do. We can't go through evdev directly because SDL is how the Steam controller mapping feature is implemented. FWIW the SDL devs are friendly if you'd like to give this a shot yourself, I don't know when we'll have time to tackle this.

kakra commented 4 years ago

Yeah, I can create a PR for SDL... Just "hg" and me are not particularly friends. ;-) I can also make a patch for Proton, should I send it to you for review?

kakra commented 4 years ago

@aeikum https://bugzilla.libsdl.org/show_bug.cgi?id=5241

kakra commented 4 years ago

@aeikum I wonder what the impact of the Steam controller handler (I don't know how you call it exactly) is with which you can adjust deadzones and mappings for all supported controllers (not just original Steam controllers). Does it also add another set of deadzones? On top of what SDL already provides? Plus the deadzones that xinput games add by themselves? That would result in 3 deadzones applied on top of each other and could explain the lag behavior some people describe.

I always disable this feature because using this has a strange feeling to the game controls I cannot exactly describe but it may be due to the deadzones.

kakra commented 4 years ago

@aeikum The patch has been accepted into SDL: https://bugzilla.libsdl.org/show_bug.cgi?id=5241

How do we proceed from here? I think we can get/set the hint either from the proton runner or from Wine xinput? While playing around a little with a newly ordered Thrustmaster HOTAS, I found that the hint would also apply to non-xinput devices like this HOTAS while it should not. So we'd need to enable Wine-internal dead zone handler when this hint is set, so we end up without dead zones only in the xinput path, right? I'm not sure what games do expect from the standard joystick interface (non-xinput).

aeikum commented 4 years ago

It would've been nice if the switch was per-handle, or per-device, instead of global. Then we could have deadzones on for dinput but off for xinput. However, I think dinput already has some deadzone smarts, maybe you can dig into that and figure out how it ought to behave.

kakra commented 4 years ago

Yeah, I was hoping that the patch would receive a review first about that matter instead of being merged. But I think this is an edge case for Wine only as Wine needs to handle different device classes all at once, while single games using SDL don't.

I already dug into the code and I think it should be possible to just turn dead zones off in SDL globally, then activate the dead zones handler built into the dinput code path of Wine. I may need some review from you on the patches. And when they are ready, we decide which patch should go into mainline, and which stays in Proton only.

There seem to be three different drivers: xinput (has no dead zones handler), dinput and winejoystick.sys? And the latter seems to be the only one using the joydev interface from what I remember. But I'm not sure if there's dead code in Wine, or some code only applies for some very special or old API cases. Maybe you can shed a quick light on that? We only need to look at code that uses SDL... Code that uses joydev is not affected by the SDL patch.

aeikum commented 4 years ago

winejoystick is not dead code, it's the drivers for the winmm joystick interfaces, but you can ignore it for now. It needs to be re-written on top of HID. See proton.git/docs/CONTROLLERS.md for a little more info.

I think it should be possible to just turn dead zones off in SDL globally, then activate the dead zones handler built into the dinput code path of Wine

I haven't looked into it at all, but this sounds right to me. Upstream doesn't have an SDL handler for dinput, so for now, I think it can be Proton-only. dinput also needs to be rewritten on top of HID, and when that's done, we should handle deadzones there.

kakra commented 3 years ago

@aeikum Next SDL2 version is probably going to ship with deadzones off by default, so Proton will see unfiltered values as if it would query the hardware directly - exactly what we need. Thus, you may need to ship Proton with this newer version then: https://bugzilla.libsdl.org/show_bug.cgi?id=5241#c17

I revisited games using my HOTAS, and having no deadzone from SDL2 is actually correct, otherwise some of the input controls are flawed (i.e. dials and throttles which have no center would have a deadzone in the center). Instead, one needs to adjust the deadzone in the game options per input.

aeikum commented 3 years ago

We use the SDL shipped in the Steam Linux Runtime, so the change will come in through that. No changes are needed to Wine, correct?

kakra commented 3 years ago

I don't expect any changes to be needed. Should we need any patches, I'll send a PR.