a) Some common mistakes about linux directions: - using 0x7FFF as ~359 degrees for linux direction, instead of 0xFFFF (look at /include/uapi/linux/input.h:1117) - the everlasting confusion about how to map dinput to linux directions, this proves to be a difficult topic, only by finding out that the SDL2 haptic implementation does a different conversion than Wine does, and since only one possibility is the right one, at least one of the implementations is wrong. It turns out they're both wrong. With this explanation, I try to finally come up with the right implementation, by deriving it in a systematical way. This explanation has been verified by the linux input mailing lists: http://www.mail-archive.com/linux-input@vger.kernel.org/msg08459.html
Directions ==========
For conversions between different APIs, the physical behaviour should be the same.
DInput ______
Extracted from http://msdn.microsoft.com/en-us/library/windows/desktop/ee417536%28v=vs.85%2... :
Physical situation:
North (polar 0, spher[0] 270) (0, -1) -y Far
West East (polar 90, spher[0] 0) (-1, 0) O (+1, 0) -x Left +x Right
South (0, +1) +y Near
.------. | User | '------'
Remember these are the 'directions': as defined as the direction the user should exert force in to counteract the force. So the direction in which the force is applied by the joystick, is opposite.
If we're interested in the direction in which the force is applied by the joystick, the physical situation becomes point-flipped:
(polar 0, spher[0] 270) (0, +1) +y
(polar 270, spher[0] 180) (polar 90, spher[0] 0) (+1, 0) O (-1, 0) +x -x
(polar 180, spher[0] 90) (0, -1) -y
Linux _____
Extracted from Linux /include/uapi/linux/input.h:1113, and from the discussion with Anssi Hannula in the mailing lists :
Physical situation:
Up (dir 180) (0, -1) -y
Left (dir 90) Right (dir 270) (-1, 0) O (+1, 0) -x +x
Down (dir 0) (0, +1) +y
.------. | User | '------'
It is unclear what is meant by 'directions' in this case: Possibility #1: counteraction direction of user Possibility #2: direction of force applied by joystick
Anssi Hannula confirmed that Possibility #2 is the right one.
Mapping _______
In the "Carts" column, you can find the Cartesian coordinates which indicate the counteraction direction of user. (This choice is arbitrary ('direction of force applied by joystick' could also have been chosen), just to have a physical reference.) These "Carts" tuples should be equal across APIs to ensure equal behavior.
#Assume Possibility #1: # # DInput <-> Linux # _________________ | ___________ # Carts|Polar|Spher | Carts|Direc # -----+-----+----- | -----+----- # 0, -1| 0 | 270 | 0, -1| 180 # +1, 0| 90 | 0 | +1, 0| 270 # 0, +1| 180 | 90 | 0, +1| 0 # -1, 0| 270 | 180 | -1, 0| 90
Assume Possibility #2: => Linux Cartesian coordinates are opposite in respect to the "Carts" reference.
DInput <-> Linux _________________ | ___________ Carts|Polar|Spher | Carts|Direc -----+-----+----- | -----+----- 0, -1| 0 | 270 | 0, -1| 0 +1, 0| 90 | 0 | +1, 0| 90 0, +1| 180 | 90 | 0, +1| 180 -1, 0| 270 | 180 | -1, 0| 270
This conversion table will be used in this document.
On effect_linuxinput.c:385, there is written: /* some of this may look funky, but it's 'cause the linux driver and directx have * different opinions about which way direction "0" is. directx has 0 along the x * axis (left), linux has it along the y axis (down). */ The part "directx has 0 along the x axis (left)" contradicts with "angle bases: 0 -> -y (down) (linux) -> 0 -> +x (right) (windows)" on line 148, because 'left' != 'right'. Also, this comment seems to be about the dinput spherical coordinates, instead of polar. Applying the conversion table on dinput/effect_linuxinput.c:385, change : /* some of this may look funky, but it's 'cause the linux driver and directx have * different opinions about which way direction "0" is. directx has 0 along the x * axis (left), linux has it along the y axis (down). */ if (dwFlags & DIEP_DIRECTION) { if (peff->cAxes == 1) { if (peff->dwFlags & DIEFF_CARTESIAN) { if (dwFlags & DIEP_AXES) { if (peff->rgdwAxes[0] == DIJOFS_X && peff->rglDirection[0] >= 0) This->effect.direction = 0x4000; else if (peff->rgdwAxes[0] == DIJOFS_X && peff->rglDirection[0] < 0) This->effect.direction = 0xC000; else if (peff->rgdwAxes[0] == DIJOFS_Y && peff->rglDirection[0] >= 0) This->effect.direction = 0; else if (peff->rgdwAxes[0] == DIJOFS_Y && peff->rglDirection[0] < 0) This->effect.direction = 0x8000; to : /* some of this may look funky, but it's 'cause the linux driver and directx have * different opinions about which way direction "0" is. directx has 0 along the y * axis (north/far, counteract), linux has it along the y axis (down). */ if (dwFlags & DIEP_DIRECTION) { if (peff->cAxes == 1) { if (peff->dwFlags & DIEFF_CARTESIAN) { if (peff->rgdwAxes[0] == DIJOFS_X && peff->rglDirection[0] >= 0) This->effect.direction = 0x4000; else if (peff->rgdwAxes[0] == DIJOFS_X && peff->rglDirection[0] < 0) This->effect.direction = 0xC000; else if (peff->rgdwAxes[0] == DIJOFS_Y && peff->rglDirection[0] >= 0) This->effect.direction = 0x8000; else if (peff->rgdwAxes[0] == DIJOFS_Y && peff->rglDirection[0] < 0) This->effect.direction = 0;
Change dinput/effect_linuxinput.c:148 : /* Major conversion factors are: * times: millisecond (linux) -> microsecond (windows) (x * 1000) * forces: scale 0x7FFF (linux) -> scale 10000 (windows) approx ((x / 33) * 10) * angles: scale 0x7FFF (linux) -> scale 35999 (windows) approx ((x / 33) * 36) * angle bases: 0 -> -y (down) (linux) -> 0 -> +x (right) (windows) */ to : /* Major conversion factors are: * times: millisecond (linux) -> microsecond (windows) (x * 1000) * forces: scale 0x7FFF (linux) -> scale 10000 (windows) approx ((x / 33) * 10) * angles: scale 0xFFFF (linux) -> scale 35999 (windows) approx ((x / 66) * 36) * angle bases: 0 -> +y (down) (linux) -> 0 -> -y (far: counteract direction) (windows polar) */
and change dinput/effect_linuxinput.c:184 : peff->rglDirection[0] = (This->effect.direction / 33) * 36 + 9000; if (peff->rglDirection[0] > 35999) peff->rglDirection[0] -= 35999; to : peff->rglDirection[0] = (This->effect.direction / 66) * 36;
and change dinput/effect_linuxinput.c:415 : This->effect.direction = (int)((3 * M_PI / 2 - atan2(y, x)) * -0x7FFF / M_PI); to : This->effect.direction = (unsigned int)((M_PI / 2 + atan2(y, x)) * 0x8000 / M_PI);
and change dinput/effect_linuxinput.c:505 : /* One condition block. This needs to be rotated to direction, * and expanded to separate x and y conditions. */ int i; double factor[2]; factor[0] = asin((This->effect.direction * 3.0 * M_PI) / 0x7FFF); factor[1] = acos((This->effect.direction * 3.0 * M_PI) / 0x7FFF); to : /* One condition block. This needs to be rotated to direction, * using dinput Cartesian coordinates, and expanded to separate x and y conditions. */ int i; double factor[2]; factor[0] = +sin((This->effect.direction * M_PI) / 0x8000); factor[1] = -cos((This->effect.direction * M_PI) / 0x8000);
b) Applying direction seems to have no influence on X and Y force. (It's always polar 0 degrees) Tested in FEdit => Multiple things in the implementation are wrong: - typo in the bracket placement for the double-to-int conversion - forgetting (typo) that dinput works with 9000 instead of 90 (inconsistent with linux->dinput conversion of GetParameters) - common mistakes about linux directions, see above - it is more readable and accurate to use "* (0x8000 / 18000)" than "* (0xFFFF / 35999)", and wrapping of this (effect.direction) u16 is intentional. Solution: Change dinput/effect_linuxinput.c:419 : This->effect.direction = (int)(((double)peff->rglDirection[0] - 90) / 35999) * 0x7FFF; to : This->effect.direction = (unsigned int)(((double)peff->rglDirection[0] / 18000) * 0x8000);
c) Multiple things are weird/wrong here: - common mistakes about linux directions, see above - "M_PI * 3", why '3' ??? - Why "1000" as max magnitude of direction? type(rglDirection[i]) == 'LONG' => You can pick at least 10000 (DI_FFNOMINALMAX). So, change dinput/effect_linuxinput.c:174 : if (peff->dwFlags & DIEFF_CARTESIAN) { peff->rglDirection[0] = sin(M_PI * 3 * This->effect.direction / 0x7FFF) * 1000; peff->rglDirection[1] = cos(M_PI * 3 * This->effect.direction / 0x7FFF) * 1000; to : if (peff->dwFlags & DIEFF_CARTESIAN) { peff->rglDirection[0] = +sin(This->effect.direction * M_PI / 0x8000) * DI_FFNOMINALMAX; peff->rglDirection[1] = -cos(This->effect.direction * M_PI / 0x8000) * DI_FFNOMINALMAX;
d) The statement "Polar and spherical are the same for 2 axes" is wrong: http://msdn.microsoft.com/en-us/library/windows/desktop/ee417536%28v=vs.85%2... : "Polar coordinates are expressed as a single angle, in hundredths of a degree clockwise from whatever zero-point, or true north, has been established for the effect. Normally this is the negative y-axis; that is, away from the user." => (0, -1) direction is Polar starting direction (angle = 0) http://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sd... : "If spherical, the first angle is measured in hundredths of a degree from the (1, 0) direction, rotated in the direction of (0, 1)." => (1, 0) direction is Spherical starting direction (angle = 0) (also take a look at the conversion table)
So, change dinput/effect_linuxinput.c:177 : } else { /* Polar and spherical coordinates are the same for two or less * axes. * Note that we also use this case if NO flags are marked. * According to MSDN, we should return the direction in the * format that it was specified in, if no flags are marked. */ peff->rglDirection[0] = (This->effect.direction / 66) * 36; } to : } else if (peff->dwFlags & DIEFF_SPHERICAL) { peff->rglDirection[0] = 27000 + (This->effect.direction / 66) * 36; if (peff->rglDirection[0] >= 36000) peff->rglDirection[0] -= 36000; } else /* if (peff->dwFlags & DIEFF_POLAR) */ { /* Note that we return polar coordinates if NO flags are marked. * According to MSDN, we should return the direction in the * format that it was specified in, if no flags are marked. * TODO: Return the direction in the format that it was specified in, * if no flags are marked. */ peff->rglDirection[0] = (This->effect.direction / 66) * 36; }
And change dinput/effect_linuxinput.c:416 : } else { /* Polar and spherical are the same for 2 axes */ /* Precision is important here, so we do double math with exact constants */ This->effect.direction = (unsigned int)(((double)peff->rglDirection[0] / 18000) * 0x8000); } to : } else if (peff->dwFlags & DIEFF_SPHERICAL) { This->effect.direction = (unsigned int)(((9000 + (double)peff->rglDirection[0]) / 18000) * 0x8000); } else /* if (peff->dwFlags & DIEFF_POLAR) */ { This->effect.direction = (unsigned int)(((double)peff->rglDirection[0] / 18000) * 0x8000); }
e) If "first_axis_is_x" in effect_linuxinput.c happens to be "false" and "cAxes == 2", doesn't the assignment of "effect.direction" in SetParameters(...) need additional transformations if "(peff->dwFlags & DIEFF_POLAR) != 0"? Because then x and y axis are swapped. With swapped x and y axes, then the following conversions hold:
#Assume Possibility #1: # # DInput <-> Linux # _________________ | ___________ # Carts|Polar|Spher | Carts|Direc # -----+-----+----- | -----+----- # -1, 0| 0 | 270 | -1, 0| 90 # 0, +1| 90 | 0 | 0, +1| 0 # +1, 0| 180 | 90 | +1, 0| 270 # 0, -1| 270 | 180 | 0, -1| 180
Assume Possibility #2: => Linux Cartesian coordinates are opposite in respect to the "Carts" reference.
DInput <-> Linux _________________ | ___________ Carts|Polar|Spher | Carts|Direc -----+-----+----- | -----+----- -1, 0| 0 | 270 | -1, 0| 270 0, +1| 90 | 0 | 0, +1| 180 +1, 0| 180 | 90 | +1, 0| 90 0, -1| 270 | 180 | 0, -1| 0
(Possibility #2 is the correct one)
So change dinput/effect_linuxinput.c:416 : } else if (peff->dwFlags & DIEFF_SPHERICAL) { This->effect.direction = (unsigned int)(((9000 + (double)peff->rglDirection[0]) / 18000) * 0x8000); } else /* if (peff->dwFlags & DIEFF_POLAR) */ { This->effect.direction = (unsigned int)(((double)peff->rglDirection[0] / 18000) * 0x8000); } to : } else if (peff->dwFlags & DIEFF_SPHERICAL) { This->effect.direction = (unsigned int)(((9000 + (double)peff->rglDirection[0]) / 18000) * 0x8000); if (!This->first_axis_is_x) This->effect.direction = 0xC000 - This->effect.direction; } else /* if (peff->dwFlags & DIEFF_POLAR) */ { This->effect.direction = (unsigned int)(((double)peff->rglDirection[0] / 18000) * 0x8000); if (!This->first_axis_is_x) This->effect.direction = 0xC000 - This->effect.direction; }
Then we can simplify the following code of dinput/effect_linuxinput.c:405 : } else { /* two axes */ if (peff->dwFlags & DIEFF_CARTESIAN) { LONG x, y; if (This->first_axis_is_x) { x = peff->rglDirection[0]; y = peff->rglDirection[1]; } else { x = peff->rglDirection[1]; y = peff->rglDirection[0]; } This->effect.direction = (unsigned int)((M_PI / 2 + atan2(y, x)) * 0x8000 / M_PI); } else if (peff->dwFlags & DIEFF_SPHERICAL) { This->effect.direction = (unsigned int)(((9000 + (double)peff->rglDirection[0]) / 18000) * 0x8000); if (!This->first_axis_is_x) This->effect.direction = 0xC000 - This->effect.direction; } else /* if (peff->dwFlags & DIEFF_POLAR) */ { This->effect.direction = (unsigned int)(((double)peff->rglDirection[0] / 18000) * 0x8000); if (!This->first_axis_is_x) This->effect.direction = 0xC000 - This->effect.direction; } } to : } else { /* two axes */ if (peff->dwFlags & DIEFF_CARTESIAN) { This->effect.direction = (unsigned int)((M_PI / 2 + atan2(peff->rglDirection[1], peff->rglDirection[0])) * 0x8000 / M_PI); } else if (peff->dwFlags & DIEFF_SPHERICAL) { This->effect.direction = (unsigned int)(((9000 + (double)peff->rglDirection[0]) / 18000) * 0x8000); } else /* if (peff->dwFlags & DIEFF_POLAR) */ { This->effect.direction = (unsigned int)(((double)peff->rglDirection[0] / 18000) * 0x8000); } if (!This->first_axis_is_x) This->effect.direction = 0xC000 - This->effect.direction; }
f) Condition effects (with struct-size of 2*sizeof(DICONDITION)) should keep "first_axis_is_x" into account: Change dinput/effect_linuxinput.c:520 : /* Two condition blocks. Direct parameter copy. */ int i; for (i = 0; i < 2; ++i) { This->effect.u.condition[i].center = (tsp[i].lOffset / 10) * 32; This->effect.u.condition[i].right_coeff = (tsp[i].lPositiveCoefficient / 10) * 32; This->effect.u.condition[i].left_coeff = (tsp[i].lNegativeCoefficient / 10) * 32; This->effect.u.condition[i].right_saturation = (tsp[i].dwPositiveSaturation / 10) * 65; This->effect.u.condition[i].left_saturation = (tsp[i].dwNegativeSaturation / 10) * 65; This->effect.u.condition[i].deadband = (tsp[i].lDeadBand / 10) * 65; } to : /* Two condition blocks. Direct parameter copy, after a small change of axes if needed. */ int i, j; for (i = 0; i < 2; ++i) { j = (first_axis_is_x ? i : 1-i); This->effect.u.condition[j].center = (tsp[i].lOffset / 10) * 32; This->effect.u.condition[j].right_coeff = (tsp[i].lPositiveCoefficient / 10) * 32; This->effect.u.condition[j].left_coeff = (tsp[i].lNegativeCoefficient / 10) * 32; This->effect.u.condition[j].right_saturation = (tsp[i].dwPositiveSaturation / 10) * 65; This->effect.u.condition[j].left_saturation = (tsp[i].dwNegativeSaturation / 10) * 65; This->effect.u.condition[j].deadband = (tsp[i].lDeadBand / 10) * 65; }
The condition effects with struct-size of 1*sizeof(DICONDITION) don't need changes regarding "first_axis_is_x", because "factor[i]" depends on "effect.direction", which already incorporates "first_axis_is_x".
g) Condition effects (with struct-size of 1*sizeof(DICONDITION)) are not transformed exactly right, look at "interactive.fig" for more understanding of the suggested code: - "deadband" can't be negative - for "left/right_coeff" and "left/right_saturation", if a direction along an axis is negative, "left" and "right" should be swapped
Change dinput/effect_linuxinput.c:511 : for (i = 0; i < 2; ++i) { This->effect.u.condition[i].center = (int)(factor[i] * (tsp->lOffset / 10) * 32); This->effect.u.condition[i].right_coeff = (int)(factor[i] * (tsp->lPositiveCoefficient / 10) * 32); This->effect.u.condition[i].left_coeff = (int)(factor[i] * (tsp->lNegativeCoefficient / 10) * 32); This->effect.u.condition[i].right_saturation = (int)(factor[i] * (tsp->dwPositiveSaturation / 10) * 65); This->effect.u.condition[i].left_saturation = (int)(factor[i] * (tsp->dwNegativeSaturation / 10) * 65); This->effect.u.condition[i].deadband = (int)(factor[i] * (tsp->lDeadBand / 10) * 65); } to : for (i = 0; i < 2; ++i) { This->effect.u.condition[i].center = (int)(factor[i] * (tsp->lOffset / 10) * 32); This->effect.u.condition[i].right_coeff = (int)(abs(factor[i]) * ((factor[i] >= 0 ? tsp->lPositiveCoefficient : tsp->lNegativeCoefficient) / 10) * 32); This->effect.u.condition[i].left_coeff = (int)(abs(factor[i]) * ((factor[i] >= 0 ? tsp->lNegativeCoefficient : tsp->lPositiveCoefficient) / 10) * 32); This->effect.u.condition[i].right_saturation = (int)(abs(factor[i]) * ((factor[i] >= 0 ? tsp->dwPositiveSaturation : tsp->dwNegativeSaturation) / 10) * 65); This->effect.u.condition[i].left_saturation = (int)(abs(factor[i]) * ((factor[i] >= 0 ? tsp->dwNegativeSaturation : tsp->dwPositiveSaturation) / 10) * 65); This->effect.u.condition[i].deadband = (int)(abs(factor[i]) * (tsp->lDeadBand / 10) * 65); }
Elias