From 066a257c2c8319a797f3f3a276d9c94846e571c8 Mon Sep 17 00:00:00 2001 From: dwillbarron <12925039+dwillbarron@users.noreply.github.com> Date: Tue, 2 Dec 2025 16:38:18 -0600 Subject: [PATCH] DirectInput FFB: Calculate appropriate update flags Anecodtally, some force-feedback wheels have been reported to experience a reduced "definition", "texture", "precision", or "je ne sais quoi", which appears to be caused by sending more update flags than necessary to DirectInput. This may be related to the fact that there are two USB PID packets that are sent when updating a device: One contains the "general" force data, and the other contains the "type-specific" data. My speculation is that many wheels expect to only receive the latter, and misbehave when receiving both. This has been tested and validated anecdotally by others who have received a hacked-together version of PCSX2 that corrects the flags sent to DirectInput, who noted a significant improvement in the "feeling" of the FFB effects. The only way to validate this at a technical level is to grab a wheel that uses the "generic" DirectInput FFB drivers (which map nearly 1:1 with the USB PID specification), and inspect the USB packets (e.g. with USBPcap) to check whether redundant data is being sent. --- src/haptic/windows/SDL_dinputhaptic.c | 106 ++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 8 deletions(-) diff --git a/src/haptic/windows/SDL_dinputhaptic.c b/src/haptic/windows/SDL_dinputhaptic.c index 79c4b3501f0cd..68ec30526ef7d 100644 --- a/src/haptic/windows/SDL_dinputhaptic.c +++ b/src/haptic/windows/SDL_dinputhaptic.c @@ -946,6 +946,103 @@ bool SDL_DINPUT_HapticNewEffect(SDL_Haptic *haptic, struct haptic_effect *effect return false; } +BOOL DIGetDirectionUpdateFlag(DIEFFECT* before, DIEFFECT* after) +{ + if (before->cAxes != after->cAxes) { + return true; + } + // rglDirection must be non-null for DIEP_DIRECTION to be a valid flag + if (after->rglDirection == NULL) { + return false; + } + if (before->rglDirection == NULL) { + return true; + } + + return SDL_memcmp(before->rglDirection, after->rglDirection, sizeof(LONG) * before->cAxes) != 0; +} + +BOOL DIGetEnvelopeUpdateFlag(DIEFFECT* before, DIEFFECT* after) { + if (before->lpEnvelope == NULL && after->lpEnvelope == NULL) { + return false; + } + // A null lpEnvelope is valid for DIEP_ENVELOPE (clears the envelope from the effect) + if (before->lpEnvelope == NULL || after->lpEnvelope == NULL) { + return true; + } + return SDL_memcmp(before->lpEnvelope, after->lpEnvelope, sizeof(DIENVELOPE)) != 0; +} + +BOOL DIGetTypeSpecificParamsUpdateFlag(DIEFFECT* before, DIEFFECT* after) +{ + // Shouldn't happen since this implies an effect's type somehow changed, but need to check to avoid an out-of-bounds memcmp + if (before->cbTypeSpecificParams != after->cbTypeSpecificParams) { + return true; + } + // lpvTypeSpecificParams must be non-null for the DIEP_TYPESPECIFICPARAMS flag. + if (after->lpvTypeSpecificParams == NULL) { + return false; + } + if (before->lpvTypeSpecificParams == NULL) { + return true; + } + return SDL_memcmp(before->lpvTypeSpecificParams, after->lpvTypeSpecificParams, before->cbTypeSpecificParams) != 0; +} + +/* + Calculate the exact flags needed when updating an existing DirectInput haptic effect. +*/ +DWORD DICalculateUpdateFlags(DIEFFECT *before, DIEFFECT *after) +{ + DWORD flags = 0; + + if (DIGetDirectionUpdateFlag(before, after)) { + flags |= DIEP_DIRECTION; + } + + if (before->dwDuration != after->dwDuration) { + flags |= DIEP_DURATION; + } + + if (DIGetEnvelopeUpdateFlag(before, after)) { + flags |= DIEP_ENVELOPE; + } + + if (before->dwStartDelay != after->dwStartDelay) { + flags |= DIEP_STARTDELAY; + } + + if (before->dwTriggerButton != after->dwTriggerButton) { + flags |= DIEP_TRIGGERBUTTON; + } + + if (before->dwTriggerRepeatInterval != after->dwTriggerRepeatInterval) { + flags |= DIEP_TRIGGERREPEATINTERVAL; + } + + if (DIGetTypeSpecificParamsUpdateFlag(before, after)) { + flags |= DIEP_TYPESPECIFICPARAMS; + } + + if (flags == 0) { + /* Awkward: SDL_UpdateHapticEffect was called, but nothing was changed. + * Calling IDirectInputEffect_SetParameters with no flags is nonsense, + * so our options are to either send all the flags, or exit early. + * Sending all the flags seems like the safer option: The programmer may be trying + * to force an update for some reason (e.g. driver bug workaround?). Conversely, + * if the programmer doesn't want IDirectInputEffect_SetParameters to be called, they + * can just avoid calling SDL_UpdateHapticEffect when there's no changes. */ + flags = DIEP_DIRECTION | + DIEP_DURATION | + DIEP_ENVELOPE | + DIEP_STARTDELAY | + DIEP_TRIGGERBUTTON | + DIEP_TRIGGERREPEATINTERVAL | DIEP_TYPESPECIFICPARAMS; + } + + return flags; +} + bool SDL_DINPUT_HapticUpdateEffect(SDL_Haptic *haptic, struct haptic_effect *effect, const SDL_HapticEffect *data) { HRESULT ret; @@ -958,14 +1055,7 @@ bool SDL_DINPUT_HapticUpdateEffect(SDL_Haptic *haptic, struct haptic_effect *eff goto err_update; } - /* Set the flags. Might be worthwhile to diff temp with loaded effect and - * only change those parameters. */ - flags = DIEP_DIRECTION | - DIEP_DURATION | - DIEP_ENVELOPE | - DIEP_STARTDELAY | - DIEP_TRIGGERBUTTON | - DIEP_TRIGGERREPEATINTERVAL | DIEP_TYPESPECIFICPARAMS; + flags = DICalculateUpdateFlags(&effect->hweffect->effect, &temp); // Create the actual effect. ret =