Godot: Input.get_joy_axis reporting at half precision steps

Created on 17 Oct 2020  ·  14Comments  ·  Source: godotengine/godot

Godot version:
3.2.3.stable.official

OS/device including version:
Windows 10 Pro N 1903 18362.1139

Issue description:
Input.get_joy_axis method is only updating the value per 2 steps based on 0 to 255 range.
If you see the axis values in a software like XPadder, an axis is represented in 0 to 65535 range and you see the increments of 256, but Godot only updates in multiples of 512.

bug input

Most helpful comment

Hi, I'm the creator of the arcin IO board that @DJKero is using and they asked me to help track down this issue.

The arcin is sending the axis values as an 8-bit number in the range 0..255.

I started out by attempting to reproduce the problem, doing (get_joy_axis() + 1) / 2 * 255 to scale the -1..1 range back to the original 0..255 range and found it to be moving in steps of three, but sometimes counting 0, 3, 6… and other times counting 1, 4, 7… and so on.

Going through the code, I found the following snippet in InputDefault::joy_axis():

    if (p_value.value > joy.last_axis[p_axis]) {

        if (p_value.value < joy.last_axis[p_axis] + joy.filter) {

            return;
        }
    } else if (p_value.value > joy.last_axis[p_axis] - joy.filter) {

        return;
    }

The value here is scaled to a 0..1 range, and joy.filter is hardcoded to 0.01. Since a single step is a little bit smaller than 0.004 in this scale, any update event with a smaller difference than three steps from the last reported value is dropped, which is consistent with the behavior I see. Changing joy.filter to 0 lets single steps through.

In other words, this is a feature, not a bug, presumably added to get rid of jitter from noisy joysticks. The problem is that it sacrifices precision and the filter threshold is as far as I can see hardcoded in the source, so it can't be changed by users needing the full precision.

All 14 comments

cc @madmiraal

I'm struggling to understand the issue. Input.get_joy_axis() returns a float between -1 and 1 not a discrete value in multiples of 512.

On Windows this value is extracted using the Windows API's XInputGetState function, which uses the XINPUT_GAMEPAD structure. As stated in the documentation, this returns values between 0 and 255 for triggers and -32768 and 32767 for the left and right, x- and y-axes i.e. a range of 65535. Godot stores these range values as constants:
https://github.com/godotengine/godot/blob/9dad483920ffa3c32a03f1f4cc869b3f7d1cbe0e/platform/windows/joypad_windows.h#L69
https://github.com/godotengine/godot/blob/9dad483920ffa3c32a03f1f4cc869b3f7d1cbe0e/platform/windows/joypad_windows.h#L66
It converts the values returned by the OS to a float by dividing the values by these constants:
https://github.com/godotengine/godot/blob/9dad483920ffa3c32a03f1f4cc869b3f7d1cbe0e/platform/windows/joypad_windows.cpp#L457-L473
At no stage does it use multiples of 512.

Let me try to explain myself, sorry if I'm not explaining it well enough by the way.

The issue I'm experiencing is that it seems that the method isn't reflecting the changes in the internal value (-32768 to 32767) when it changes in steps of 256.
For a device with 0-255 precision, 1 of its steps would be a change around 256, however the method doesn't reflect that in the value returned, it only updates when the device moves 2 steps from a correctly detected state.

Edit:
I'll ask the guy that made this PCB if he can modify the firmware to use the triggers instead of the analog sticks to check if this behaviour repeats with those too.

An example just in case:

When sThumbLX is -256 the method should return -0.0078xx but it returns 0.
When sThumbLX is -512 the method should return -0.015625 and it returns -0.015625.
When sThumbLX is -768 the method should return -0.0234xx but it returns -0.015625.
When sThumbLX is -1024 the method should return -0.03125 and it returns -0.03125.

You get the idea, hah

@DJKero If what you're suggesting were true, then, when sThumbLX was at the negative limit i.e. -32768, the value Godot would return would be -0.5, but it doesn't, it returns -1; as using any joystick will show.

This leads me to believe that the values of sThumbLX are not what you assert, and that the problem lies with how you're setting the values for sThumbLX.

No, because:
-32768 / MAX_JOY_AXIS equals -1.0

The issue is that Godot only updates the returning value half of the time, it's not updating the value when sThumbLX changes by 256 and its not a multiple of 512, read the examples I made before carefully, you cannot test this with a common joystick.

I don't know how they work but maybe this could be to tested this with a driving wheel?

Edit:
Another example:
-32.512 / MAX_JOY_AXIS should equal -0.9921875 but Godot still reports -1

Yeah you can reproduce this even with a cheapo steering wheel and some software that lets you see the value of the X axis like XPadder.

This won't affect analog sticks because of their lack of precision probably, but for other devices, we're losing precision.

Edit:
Tried with my 8bitdo SF30 and it's steps are of 512 to 700+ and even if it happens to miss an update with the movements being so small you're probably going to miss it with a joystick due to not being able to move the stick that precisely.

Something is not right with the values provided. I suspect the data from Xpadder may not be correct. Since neither Windows nor Xpadder provide their source code, I've looked at the Wine implementation. They use the following code to convert hardware joystick data to a short:

 327 static SHORT scale_short(LONG value, const struct axis_info *axis)
 328 {
 329     return ((((ULONGLONG)(value - axis->min)) * 0xffff) / axis->range) - 32768;
 330 }

Converting byte data to a short i.e. to get 0 to map to -32768 and 255 to map to 32767, results in the correct step size of 257, not 256, and there should be no 0 value:

Byte |   | Short |   | Float
-- | -- | -- | -- | --
126 | maps to | -386 | converts to | -0.0118
127 | maps to | -129 | converts to | -0.0039
128 | maps to | 128 | converts to | 0.0039
129 | maps to | 385 | converts to | 0.0117

Note: The 0.0039 value is the correct axis value you see in the Joypads Demo, when the joystick is at rest and the main reason why a dead zone is required:
joy_pad_demo

Obviously, it's possible that hardware provides different axis->min and axis->range information, but I suspect most hardware is limited to providing byte data.

Note: It's not uncommon to get the conversion from bytes to shorts wrong as this Wine commit reveals from an earlier implementation.

I don't understand what that algorithm does, sorry.
I'll check with other software like XPadder to double check the values.

2020-10-30_16-00-47
2020-10-30_16-00-52
2020-10-30_16-00-57
2020-10-30_16-01-03
2020-10-30_16-01-17

Here's proof there's something wrong: (Tested it here)
Source code of that webpage can be found here. It uses this API.

godot windows opt tools 64_2020-10-30_16-12-50
2020-10-30_16-13-04
2020-10-30_16-13-15
2020-10-30_16-13-29
2020-10-30_16-13-37
2020-10-30_16-13-46
2020-10-30_16-14-00

Hi, I'm the creator of the arcin IO board that @DJKero is using and they asked me to help track down this issue.

The arcin is sending the axis values as an 8-bit number in the range 0..255.

I started out by attempting to reproduce the problem, doing (get_joy_axis() + 1) / 2 * 255 to scale the -1..1 range back to the original 0..255 range and found it to be moving in steps of three, but sometimes counting 0, 3, 6… and other times counting 1, 4, 7… and so on.

Going through the code, I found the following snippet in InputDefault::joy_axis():

    if (p_value.value > joy.last_axis[p_axis]) {

        if (p_value.value < joy.last_axis[p_axis] + joy.filter) {

            return;
        }
    } else if (p_value.value > joy.last_axis[p_axis] - joy.filter) {

        return;
    }

The value here is scaled to a 0..1 range, and joy.filter is hardcoded to 0.01. Since a single step is a little bit smaller than 0.004 in this scale, any update event with a smaller difference than three steps from the last reported value is dropped, which is consistent with the behavior I see. Changing joy.filter to 0 lets single steps through.

In other words, this is a feature, not a bug, presumably added to get rid of jitter from noisy joysticks. The problem is that it sacrifices precision and the filter threshold is as far as I can see hardcoded in the source, so it can't be changed by users needing the full precision.

I could try to help expose this through the API and learn in the process if you see fit.

The value here is scaled to a 0..1 range, and joy.filter is hardcoded to 0.01. Since a single step is a little bit smaller than 0.004 in this scale, any update event with a smaller difference than three steps from the last reported value is dropped, which is consistent with the behavior I see. Changing joy.filter to 0 lets single steps through.

@zyp Yes, I think that's it. Looks like the filter was added on the assumption that the input would be continuous and not in steps; so it's not needed.

BTW Depending on the axis, the value can also be scaled in the -1...1 range with steps of 0.008, but the principal is the same.

Thanks guys 😁

Was this page helpful?
0 / 5 - 0 ratings