diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 95aac6d9..f1c72d83 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -5611,6 +5611,37 @@ GLFWAPI const char* glfwGetGamepadName(int jid); */ GLFWAPI int glfwGetGamepadState(int jid, GLFWgamepadstate* state); +/*! @brief Sets the intensity of a joystick's rumble effect. + * + * This function sends vibration data to joysticks that implement haptic feedback + * effects using two vibration motors: a low-frequency motor, and a + * high-frequency motor. + * + * Vibration intensity is a value between 0.0 and 1.0 inclusive, where 0.0 is no + * vibration, and 1.0 is maximum vibration. It is set separately for the + * joystick's low frequency and high frequency rumble motors. + * + * If the specified joystick is not present or does not support the rumble effect, + * this function will return `GLFW_FALSE` but will not generate an error. + * + * @param[in] jid The [joystick](@ref joysticks) to vibrate. + * @param[in] slowMotorIntensity The low frequency vibration intensity. + * @param[in] fastMotorIntensity The high frequency vibration intensity. + * @return `GLFW_TRUE` if successful, or `GLFW_FALSE` if no joystick is connected, + * or the joystick does not support the rumble effect. + * + * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref + * GLFW_INVALID_ENUM. + * + * @thread_safety This function must only be called from the main thread. + * + * @note @win32 This function is only implemented for XInput devices. + * @note @macos This function is not implemented. + * + * @ingroup input + */ +GLFWAPI int glfwSetJoystickRumble(int jid, float slowMotorIntensity, float fastMotorIntensity); + /*! @brief Sets the clipboard to the specified string. * * This function sets the system clipboard to the specified, UTF-8 encoded diff --git a/src/cocoa_joystick.m b/src/cocoa_joystick.m index a8081d2c..56646d9d 100644 --- a/src/cocoa_joystick.m +++ b/src/cocoa_joystick.m @@ -481,3 +481,8 @@ void _glfwPlatformUpdateGamepadGUID(char* guid) } } +int _glfwPlatformSetJoystickRumble(_GLFWjoystick* js, float slowMotorIntensity, float fastMotorIntensity) +{ + return GLFW_FALSE; +} + diff --git a/src/input.c b/src/input.c index 83ef6921..d1d9da1c 100644 --- a/src/input.c +++ b/src/input.c @@ -1362,6 +1362,34 @@ GLFWAPI int glfwGetGamepadState(int jid, GLFWgamepadstate* state) return GLFW_TRUE; } +GLFWAPI int glfwSetJoystickRumble(int jid, float slowMotorIntensity, float fastMotorIntensity) +{ + _GLFWjoystick* js; + + assert(jid >= GLFW_JOYSTICK_1); + assert(jid <= GLFW_JOYSTICK_LAST); + + _GLFW_REQUIRE_INIT_OR_RETURN(GLFW_FALSE); + + if (jid < 0 || jid > GLFW_JOYSTICK_LAST) + { + _glfwInputError(GLFW_INVALID_ENUM, "Invalid joystick ID %i", jid); + return GLFW_FALSE; + } + + js = _glfw.joysticks + jid; + if (!js->present) + return GLFW_FALSE; + + slowMotorIntensity = slowMotorIntensity < 0.0f ? 0.0f : slowMotorIntensity; + slowMotorIntensity = slowMotorIntensity > 1.0f ? 1.0f : slowMotorIntensity; + + fastMotorIntensity = fastMotorIntensity < 0.0f ? 0.0f : fastMotorIntensity; + fastMotorIntensity = fastMotorIntensity > 1.0f ? 1.0f : fastMotorIntensity; + + return _glfwPlatformSetJoystickRumble(js, slowMotorIntensity, fastMotorIntensity); +} + GLFWAPI void glfwSetClipboardString(GLFWwindow* handle, const char* string) { assert(string != NULL); diff --git a/src/internal.h b/src/internal.h index f41db3c3..7069b7b0 100644 --- a/src/internal.h +++ b/src/internal.h @@ -642,6 +642,7 @@ GLFWbool _glfwPlatformInitJoysticks(void); void _glfwPlatformTerminateJoysticks(void); int _glfwPlatformPollJoystick(_GLFWjoystick* js, int mode); void _glfwPlatformUpdateGamepadGUID(char* guid); +int _glfwPlatformSetJoystickRumble(_GLFWjoystick* js, float slowMotorIntensity, float fastMotorIntensity); uint64_t _glfwPlatformGetTimerValue(void); uint64_t _glfwPlatformGetTimerFrequency(void); diff --git a/src/linux_joystick.c b/src/linux_joystick.c index 122bc66a..c0d96f04 100644 --- a/src/linux_joystick.c +++ b/src/linux_joystick.c @@ -122,6 +122,48 @@ static void pollAbsState(_GLFWjoystick* js) #define isBitSet(bit, arr) (arr[(bit) / 8] & (1 << ((bit) % 8))) +static void initJoystickForceFeedback(_GLFWjoystickLinux *linjs) +{ + linjs->rumble = NULL; + struct ff_effect* effect = NULL; + + char ffBits[(FF_CNT + 7) / 8] = {0}; + if (ioctl(linjs->fd, EVIOCGBIT(EV_FF, sizeof(ffBits)), ffBits) < 0) + { + return; + } + + if (isBitSet(FF_RUMBLE, ffBits)) + { + effect = malloc(sizeof(struct ff_effect)); + *effect = (struct ff_effect) + { + .type = FF_RUMBLE, + .id = -1, + .direction = 0, + .trigger = { + .button = 0, + .interval = 0 + }, + .replay = { + .length = 2000, // xinput rumble lasts ~2 seconds + .delay = 0 + }, + .u.rumble = { + .strong_magnitude = 0, + .weak_magnitude = 0 + } + }; + + if (ioctl(linjs->fd, EVIOCSFF, effect) < 0) + { + free(effect); + } else { + linjs->rumble = effect; + } + } +} + // Attempt to open the specified joystick device // static GLFWbool openJoystickDevice(const char* path) @@ -135,7 +177,7 @@ static GLFWbool openJoystickDevice(const char* path) } _GLFWjoystickLinux linjs = {0}; - linjs.fd = open(path, O_RDONLY | O_NONBLOCK); + linjs.fd = open(path, O_RDWR | O_NONBLOCK); if (linjs.fd == -1) return GLFW_FALSE; @@ -222,6 +264,8 @@ static GLFWbool openJoystickDevice(const char* path) } } + initJoystickForceFeedback(&linjs); + _GLFWjoystick* js = _glfwAllocJoystick(name, guid, axisCount, buttonCount, hatCount); if (!js) @@ -246,6 +290,8 @@ static GLFWbool openJoystickDevice(const char* path) static void closeJoystick(_GLFWjoystick* js) { close(js->linjs.fd); + if (js->linjs.rumble) + free(js->linjs.rumble); _glfwFreeJoystick(js); _glfwInputJoystick(js, GLFW_DISCONNECTED); } @@ -426,3 +472,31 @@ void _glfwPlatformUpdateGamepadGUID(char* guid) { } +int _glfwPlatformSetJoystickRumble(_GLFWjoystick* js, float slowMotorIntensity, float fastMotorIntensity) +{ + _GLFWjoystickLinux *linjs = &js->linjs; + + if (!js->linjs.rumble) + return GLFW_FALSE; + + js->linjs.rumble->u.rumble = (struct ff_rumble_effect) + { + .strong_magnitude = 65535 * slowMotorIntensity, + .weak_magnitude = 65535 * fastMotorIntensity + }; + + struct input_event play = + { + .type = EV_FF, + .code = linjs->rumble->id, + .value = 1 + }; + + if (ioctl(linjs->fd, EVIOCSFF, linjs->rumble) < 0 || + write(linjs->fd, (const void*) &play, sizeof(play)) < 0) + { + return GLFW_FALSE; + } + + return GLFW_TRUE; +} diff --git a/src/linux_joystick.h b/src/linux_joystick.h index 1c3ca751..fe3ab43f 100644 --- a/src/linux_joystick.h +++ b/src/linux_joystick.h @@ -44,6 +44,7 @@ typedef struct _GLFWjoystickLinux int absMap[ABS_CNT]; struct input_absinfo absInfo[ABS_CNT]; int hats[4][2]; + struct ff_effect *rumble; } _GLFWjoystickLinux; // Linux-specific joystick API data diff --git a/src/null_joystick.c b/src/null_joystick.c index 27756a61..6404aaa9 100644 --- a/src/null_joystick.c +++ b/src/null_joystick.c @@ -51,3 +51,8 @@ void _glfwPlatformUpdateGamepadGUID(char* guid) { } +int _glfwPlatformSetJoystickRumble(_GLFWjoystick* js, float slowMotorIntensity, float fastMotorIntensity) +{ + return GLFW_FALSE; +} + diff --git a/src/win32_init.c b/src/win32_init.c index c5370230..22895058 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -131,6 +131,8 @@ static GLFWbool loadLibraries(void) GetProcAddress(_glfw.win32.xinput.instance, "XInputGetCapabilities"); _glfw.win32.xinput.GetState = (PFN_XInputGetState) GetProcAddress(_glfw.win32.xinput.instance, "XInputGetState"); + _glfw.win32.xinput.SetState = (PFN_XInputSetState) + GetProcAddress(_glfw.win32.xinput.instance, "XInputSetState"); break; } diff --git a/src/win32_joystick.c b/src/win32_joystick.c index 3ac29d1d..d7c0d0b4 100644 --- a/src/win32_joystick.c +++ b/src/win32_joystick.c @@ -751,3 +751,17 @@ void _glfwPlatformUpdateGamepadGUID(char* guid) } } +int _glfwPlatformSetJoystickRumble(_GLFWjoystick* js, float slowMotorIntensity, float fastMotorIntensity) +{ + XINPUT_VIBRATION effect; + + if (js->win32.device) + return GLFW_FALSE; + + ZeroMemory(&effect, sizeof(XINPUT_VIBRATION)); + + effect.wLeftMotorSpeed = (WORD)(65535.0f * slowMotorIntensity); + effect.wRightMotorSpeed = (WORD)(65535.0f * fastMotorIntensity); + + return (int) (XInputSetState(js->win32.index, &effect) == ERROR_SUCCESS); +} diff --git a/src/win32_platform.h b/src/win32_platform.h index a16047ae..d155eb04 100644 --- a/src/win32_platform.h +++ b/src/win32_platform.h @@ -222,8 +222,10 @@ typedef DWORD (WINAPI * PFN_timeGetTime)(void); // xinput.dll function pointer typedefs typedef DWORD (WINAPI * PFN_XInputGetCapabilities)(DWORD,DWORD,XINPUT_CAPABILITIES*); typedef DWORD (WINAPI * PFN_XInputGetState)(DWORD,XINPUT_STATE*); +typedef DWORD (WINAPI * PFN_XInputSetState)(DWORD,XINPUT_VIBRATION*); #define XInputGetCapabilities _glfw.win32.xinput.GetCapabilities #define XInputGetState _glfw.win32.xinput.GetState +#define XInputSetState _glfw.win32.xinput.SetState // dinput8.dll function pointer typedefs typedef HRESULT (WINAPI * PFN_DirectInput8Create)(HINSTANCE,DWORD,REFIID,LPVOID*,LPUNKNOWN); @@ -358,6 +360,7 @@ typedef struct _GLFWlibraryWin32 HINSTANCE instance; PFN_XInputGetCapabilities GetCapabilities; PFN_XInputGetState GetState; + PFN_XInputSetState SetState; } xinput; struct { diff --git a/tests/joysticks.c b/tests/joysticks.c index df000210..d290c4c5 100644 --- a/tests/joysticks.c +++ b/tests/joysticks.c @@ -58,6 +58,9 @@ static GLFWwindow* window; static int joysticks[GLFW_JOYSTICK_LAST + 1]; static int joystick_count = 0; +static float slowRumble[GLFW_JOYSTICK_LAST + 1]; +static float fastRumble[GLFW_JOYSTICK_LAST + 1]; + static void error_callback(int error, const char* description) { fprintf(stderr, "Error: %s\n", description); @@ -175,7 +178,9 @@ int main(void) struct nk_context* nk; struct nk_font_atlas* atlas; - memset(joysticks, 0, sizeof(joysticks)); + memset(joysticks, 0, sizeof(joysticks)); + memset(slowRumble, 0, sizeof(slowRumble)); + memset(fastRumble, 0, sizeof(fastRumble)); glfwSetErrorCallback(error_callback); @@ -326,6 +331,16 @@ int main(void) nk_layout_row_dynamic(nk, 30, 8); hat_widget(nk, hat); + + nk_layout_row_dynamic(nk, 30, 2); + nk_label(nk, "Slow rumble motor intensity", NK_TEXT_LEFT); + nk_label(nk, "Fast rumble motor intensity", NK_TEXT_LEFT); + + nk_layout_row_dynamic(nk, 30, 2); + slowRumble[i] = nk_slide_float(nk, 0.0f, slowRumble[i], 1.0f, 0.05f); + fastRumble[i] = nk_slide_float(nk, 0.0f, fastRumble[i], 1.0f, 0.05f); + + glfwSetJoystickRumble(joysticks[i], slowRumble[i], fastRumble[i]); } else nk_label(nk, "Joystick has no gamepad mapping", NK_TEXT_LEFT);