From c0ad98f32a1e2fbd99abe7a8d9f487023441b9c5 Mon Sep 17 00:00:00 2001 From: Arkhist Date: Wed, 17 Jun 2026 15:09:12 +0200 Subject: [PATCH 1/4] Basic FFB support for steering wheels --- src/loader/hardware/lindbergh/forceFeedback.c | 206 ++++++++++++++++-- src/loader/input/controlIniGen.c | 2 + src/loader/input/sdlInput.c | 12 + 3 files changed, 198 insertions(+), 22 deletions(-) diff --git a/src/loader/hardware/lindbergh/forceFeedback.c b/src/loader/hardware/lindbergh/forceFeedback.c index 7134878..cb54d24 100644 --- a/src/loader/hardware/lindbergh/forceFeedback.c +++ b/src/loader/hardware/lindbergh/forceFeedback.c @@ -7,11 +7,54 @@ #include "../../input/sdlInput.h" #include "../../log/log.h" + +static const int FFB_POWER_ON = 0b01; +static const int FFB_PLAY_CONTINUOUS = 0b10; + +static const int RUN_ONCE_DURATION = 100; // ms + +static int ffb_power_mode = 0; + +extern float gFFBGlobalGain; +extern float gFFBAutocenterGain; + static int effect_id = -1; +static SDL_HapticEffect current_effect; extern SDLControllers sdlJoysticks; + +static SDL_HapticEffect constant_effect; +static int constant_effect_id = -1; + +static SDL_HapticEffect friction_effect; +static int friction_effect_id = -1; + + + +void sdlFfbSetStrength(float strength) +{ + if (!sdlJoysticks.haptics[0]) + return; + if(!SDL_SetHapticGain(sdlJoysticks.haptics[0], (int)(strength*100.0f))) + log_error("\tGain Haptic Effect did not work: %s\n", SDL_GetError()); +} + + void sdlFfbInit(void) { + // initialize constant effect + memset(&constant_effect, 0, sizeof(constant_effect)); + constant_effect.type = SDL_HAPTIC_CONSTANT; + constant_effect.constant.direction.type = SDL_HAPTIC_STEERING_AXIS; + constant_effect.constant.direction.dir[0] = 1; + constant_effect.constant.attack_length = 0; + constant_effect.constant.fade_length = 0; + + memset(&friction_effect, 0, sizeof(friction_effect)); + friction_effect.type = SDL_HAPTIC_DAMPER; + friction_effect.condition.direction.type = SDL_HAPTIC_STEERING_AXIS; + friction_effect.condition.direction.dir[0] = 1; + // SDL input mode: open haptic from already-opened joysticks for (int i = 0; i < sdlJoysticks.joysticksCount && i < MAX_JOYSTICKS; ++i) { @@ -31,10 +74,12 @@ void sdlFfbInit(void) sdlJoysticks.haptics[i] = SDL_OpenHapticFromJoystick(joy); if (sdlJoysticks.haptics[i]) { + printf("Joystick %d (%s) supports %d FFB effects.\n", i, SDL_GetJoystickName(joy), SDL_GetMaxHapticEffectsPlaying(sdlJoysticks.haptics[i])); if (SDL_GetHapticFeatures(sdlJoysticks.haptics[i]) & SDL_HAPTIC_LEFTRIGHT) { log_warn("FFB: Haptic opened from joystick %d (%s)", i, SDL_GetJoystickName(joy)); - sdlFfbRumble(1.0, 1.0, 200); + sdlFfbRumble(0.2, 0.2, 200); + sdlFfbSetStrength(gFFBGlobalGain); } else { @@ -85,6 +130,7 @@ void sdlFfbRumble(float left, float right, int duration_ms) { if (!sdlJoysticks.haptics[0]) return; + SDL_HapticEffect effect; memset(&effect, 0, sizeof(effect)); effect.type = SDL_HAPTIC_LEFTRIGHT; @@ -97,6 +143,99 @@ void sdlFfbRumble(float left, float right, int duration_ms) SDL_RunHapticEffect(sdlJoysticks.haptics[0], effect_id, 1); } + +void sdlFfbConstant(int direction, float strength, uint32_t duration_ms) +{ + if (!sdlJoysticks.haptics[0]) + return; + + int should_recreate = 1; + if ((ffb_power_mode & FFB_PLAY_CONTINUOUS) && constant_effect_id >= 0) { + should_recreate = 0; + } + + if (should_recreate && constant_effect_id >= 0) + { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], constant_effect_id); + } + + constant_effect.constant.length = duration_ms; + constant_effect.constant.level = (strength * 0x7FFF)*direction; + + if (should_recreate) { + constant_effect_id = SDL_CreateHapticEffect(sdlJoysticks.haptics[0], &constant_effect); + if(!SDL_RunHapticEffect(sdlJoysticks.haptics[0], constant_effect_id, 1)) + log_error("\tConstant Haptic Effect did not run: %s\n", SDL_GetError()); + } + else { + SDL_UpdateHapticEffect(sdlJoysticks.haptics[0], constant_effect_id, &constant_effect); + } +} + +void sdlFfbFriction(float power, float coverage, uint32_t duration_ms) +{ + if (!sdlJoysticks.haptics[0]) + return; + + int should_recreate = 1; + if ((ffb_power_mode & FFB_PLAY_CONTINUOUS) && friction_effect_id >= 0) { + should_recreate = 0; + } + + if (should_recreate && friction_effect_id >= 0) + { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], friction_effect_id); + } + + friction_effect.condition.length = duration_ms; + friction_effect.condition.right_sat[0] = ((1-power) * 0xFFFF); + friction_effect.condition.left_sat[0] = ((1-power) * 0xFFFF); + friction_effect.condition.right_coeff[0] = 32767; + friction_effect.condition.left_coeff[0] = 32767; + friction_effect.condition.deadband[0] = ((1 - coverage) * 0xFFFF); + + if (should_recreate) { + friction_effect_id = SDL_CreateHapticEffect(sdlJoysticks.haptics[0], &friction_effect); + if(friction_effect_id == -1) + log_error("\tFriction Haptic Effect did not create: %s\n", SDL_GetError()); + if(!SDL_RunHapticEffect(sdlJoysticks.haptics[0], friction_effect_id, 1)) + log_error("\tFriction Haptic Effect did not run: %s\n", SDL_GetError()); + } + else { + SDL_UpdateHapticEffect(sdlJoysticks.haptics[0], friction_effect_id, &friction_effect); + } +} + +void sdlFfbCentering(float power) +{ + if (!sdlJoysticks.haptics[0]) + return; + + log_info("Autocenter power: %f\n", power); + + if(!SDL_SetHapticAutocenter(sdlJoysticks.haptics[0], (int)(power*100.0f))) + log_error("\tAutocenter Haptic Effect did not work: %s\n", SDL_GetError()); +} + +void sdlFfbStopEffect(void) +{ + if (!sdlJoysticks.haptics[0]) + return; + if (effect_id >= 0) { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], effect_id); + effect_id = -1; + } + if (constant_effect_id >= 0) { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], constant_effect_id); + constant_effect_id = -1; + } + if (friction_effect_id >= 0) { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], friction_effect_id); + friction_effect_id = -1; + } + SDL_SetHapticAutocenter(sdlJoysticks.haptics[0], 0); +} + void sdlFfbShutdown(void) { if (sdlJoysticks.haptics[0]) @@ -108,12 +247,13 @@ void sdlFfbShutdown(void) void sdlFfbDriveboard(const unsigned char *buffer, size_t count) { - - // printf("FFB driveboard: count=%zu, data:", count); - // for (size_t i = 0; i < count; ++i) { - // printf(" 0x%02x", buffer[i]); - // } - // printf("\n"); + /*if (buffer[0] != 0x80 && buffer[0] != 0x84 && buffer[0] != 0x85 && buffer[0] != 0xfd && buffer[0] != 0x86 && buffer[0] != 0x8b && buffer[0] != 0xfb){ + printf("FFB driveboard: count=%zu, data:", count); + for (size_t i = 0; i < count; ++i) { + printf(" 0x%02x", buffer[i]); + } + printf("\n"); + }*/ switch (buffer[0]) { @@ -121,11 +261,25 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) { // power if (buffer[2] == 0x01) { - log_info("0x80 command: Triggering full-power rumble\n"); - // ffb_rumble(1.0f, 1.0f, 500); + if (buffer[1] == 0x00) + ffb_power_mode = FFB_POWER_ON | FFB_PLAY_CONTINUOUS; + else + ffb_power_mode = FFB_POWER_ON; + log_info("0x80 command: Setting the power level of FFB: %d\n", ffb_power_mode); + } + else + { + ffb_power_mode = 0; + sdlFfbStopEffect(); + log_info("0x80 command: Setting the power level of FFB: %d\n", ffb_power_mode); } break; } + case 0x83: + { // Strength of motors + float strength = ((float)buffer[1]) / 127.0f; + sdlFfbSetStrength(strength*gFFBGlobalGain); + } case 0x85: { // Rumble/vibrate // uint8_t speed = buffer[1]; @@ -134,7 +288,7 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) float force = (float)power / 63.0f; // Map power 1-63 to 0.0-1.0 int duration = 100; - log_info("Triggering rumble: force=%.2f, duration=%dms\n", force, duration); + log_info("0x85 command: Triggering rumble: force=%.2f, duration=%dms\n", force, duration); sdlFfbRumble(force, force, duration); break; } @@ -143,29 +297,35 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) uint8_t direction = buffer[1]; uint8_t value = buffer[2]; - float left = 0.0f, right = 0.0f; - int duration = 100; // ms + float strength = 0.0f; + uint32_t duration = ffb_power_mode & FFB_PLAY_CONTINUOUS ? SDL_HAPTIC_INFINITY : RUN_ONCE_DURATION; if (direction == 0x00) { // right - value from 0x7F (min) to 0x01 (max) - - right = (127.0f - (float)value) / 127.0f; + strength = (127.0f - (float)value) / 127.0f; } else if (direction == 0x01) { // left - value goes from 0x00 (min) to 0x7F (max) - left = (float)value / 127.0f; + strength = (float)value / 127.0f; } - log_info("Triggering movement rumble: left=%.2f, right=%.2f, duration=%dms\n", left, right, duration); - sdlFfbRumble(left, right, duration); + log_info("0x84 command: Triggering movement: direction=%d, strength=%.2f, duration=%dms\n", direction, strength, duration); + sdlFfbConstant(((int)direction)*2 - 1, strength, duration); break; } case 0x86: // Friction + { log_info("Friction:"); - printf(" Power: %d", buffer[1]); - printf(" / Percentage: %d\n", buffer[2]); + float power = ((float)buffer[1])/127.0f; + float coverage = ((float)buffer[2])/127.0f; + uint32_t duration = ffb_power_mode & FFB_PLAY_CONTINUOUS ? SDL_HAPTIC_INFINITY : RUN_ONCE_DURATION; + + log_info("0x86 command: Damper: strength=%.2f, coverage=%.2f\n", power, coverage); + + sdlFfbFriction(power, coverage, duration); break; + } case 0x8: break; case 0x87: // move and set target point @@ -175,10 +335,12 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) printf("Move to current target point\n"); break; case 0x8B: // centering strength - printf("Centering strength:"); - printf(" param1: %d", buffer[1]); - printf(" / param2: %d\n", buffer[2]); + { + float power = ((float)buffer[1])/127.0f; + log_info("0x8b command: centering: strength=%.2f\n", power); + sdlFfbCentering(power*gFFBAutocenterGain); break; + } case 0xFB: { // Playback sud package diff --git a/src/loader/input/controlIniGen.c b/src/loader/input/controlIniGen.c index 1514c49..9d1ed1f 100644 --- a/src/loader/input/controlIniGen.c +++ b/src/loader/input/controlIniGen.c @@ -405,6 +405,8 @@ int createDefaultControlsIni(const char *fileName) iniSetValue(ini, "Config", "ShakeIncreaseRate", "1.0"); iniSetValue(ini, "Config", "ShakeDecayRate", "0.95"); iniSetValue(ini, "Config", "ShakeMinScreenFraction", "0.15"); + iniSetValue(ini, "Config", "FFBGlobalGain", "1"); + iniSetValue(ini, "Config", "FFBAutocenterGain", "1"); iniSetValue(ini, "Config", "CardInsert_Toggle", "1"); iniSetValue(ini, "Config", "Card1Insert_Toggle", "1"); iniSetValue(ini, "Config", "Card2Insert_Toggle", "1"); diff --git a/src/loader/input/sdlInput.c b/src/loader/input/sdlInput.c index e75c210..348a24f 100644 --- a/src/loader/input/sdlInput.c +++ b/src/loader/input/sdlInput.c @@ -42,6 +42,8 @@ float gShakeValue[MAX_ENTITIES]; float gShakeIncreaseRate = 10.0f; float gShakeDecayRate = 0.95f; float gShakeMinScreenFraction = 0.15f; +float gFFBGlobalGain = 1.0f; +float gFFBAutocenterGain = 1.0f; ComboGroup gComboGroups[MAX_COMBINATION_GROUPS] = {0}; int gNumComboGroups = 0; @@ -1357,6 +1359,16 @@ void loadGlobalConfig(const IniConfig *ini) gShakeMinScreenFraction = atof(value); printf(" Set ShakeMinScreenFraction to %f\n", gShakeMinScreenFraction); } + else if (strcmp(key, "FFBGlobalGain") == 0) + { + gFFBGlobalGain = atof(value); + printf(" Set FFBGlobalGain to %f\n", gFFBGlobalGain); + } + else if (strcmp(key, "FFBAutocenterGain") == 0) + { + gFFBAutocenterGain = atof(value); + printf(" Set FFBAutocenterGain to %f\n", gFFBAutocenterGain); + } } } } From b4218f978b5340bd2c470ab6b78f453ab801e9f4 Mon Sep 17 00:00:00 2001 From: Arkhist Date: Thu, 18 Jun 2026 01:51:26 +0200 Subject: [PATCH 2/4] Add FFB Rumble Gain setting --- src/loader/hardware/lindbergh/forceFeedback.c | 6 ++++-- src/loader/input/controlIniGen.c | 1 + src/loader/input/sdlInput.c | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/loader/hardware/lindbergh/forceFeedback.c b/src/loader/hardware/lindbergh/forceFeedback.c index cb54d24..e7da1b6 100644 --- a/src/loader/hardware/lindbergh/forceFeedback.c +++ b/src/loader/hardware/lindbergh/forceFeedback.c @@ -17,6 +17,7 @@ static int ffb_power_mode = 0; extern float gFFBGlobalGain; extern float gFFBAutocenterGain; +extern float gFFBRumbleGain; static int effect_id = -1; static SDL_HapticEffect current_effect; @@ -78,7 +79,7 @@ void sdlFfbInit(void) if (SDL_GetHapticFeatures(sdlJoysticks.haptics[i]) & SDL_HAPTIC_LEFTRIGHT) { log_warn("FFB: Haptic opened from joystick %d (%s)", i, SDL_GetJoystickName(joy)); - sdlFfbRumble(0.2, 0.2, 200); + sdlFfbRumble(0.1, 0.1, 200); sdlFfbSetStrength(gFFBGlobalGain); } else @@ -279,6 +280,7 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) { // Strength of motors float strength = ((float)buffer[1]) / 127.0f; sdlFfbSetStrength(strength*gFFBGlobalGain); + break; } case 0x85: { // Rumble/vibrate @@ -289,7 +291,7 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) int duration = 100; log_info("0x85 command: Triggering rumble: force=%.2f, duration=%dms\n", force, duration); - sdlFfbRumble(force, force, duration); + sdlFfbRumble(force*gFFBRumbleGain, force*gFFBRumbleGain, duration); break; } case 0x84: diff --git a/src/loader/input/controlIniGen.c b/src/loader/input/controlIniGen.c index 9d1ed1f..8913f66 100644 --- a/src/loader/input/controlIniGen.c +++ b/src/loader/input/controlIniGen.c @@ -407,6 +407,7 @@ int createDefaultControlsIni(const char *fileName) iniSetValue(ini, "Config", "ShakeMinScreenFraction", "0.15"); iniSetValue(ini, "Config", "FFBGlobalGain", "1"); iniSetValue(ini, "Config", "FFBAutocenterGain", "1"); + iniSetValue(ini, "Config", "FFBRumbleGain", "1"); iniSetValue(ini, "Config", "CardInsert_Toggle", "1"); iniSetValue(ini, "Config", "Card1Insert_Toggle", "1"); iniSetValue(ini, "Config", "Card2Insert_Toggle", "1"); diff --git a/src/loader/input/sdlInput.c b/src/loader/input/sdlInput.c index 348a24f..054519d 100644 --- a/src/loader/input/sdlInput.c +++ b/src/loader/input/sdlInput.c @@ -44,6 +44,7 @@ float gShakeDecayRate = 0.95f; float gShakeMinScreenFraction = 0.15f; float gFFBGlobalGain = 1.0f; float gFFBAutocenterGain = 1.0f; +float gFFBRumbleGain = 1.0f; ComboGroup gComboGroups[MAX_COMBINATION_GROUPS] = {0}; int gNumComboGroups = 0; @@ -1369,6 +1370,11 @@ void loadGlobalConfig(const IniConfig *ini) gFFBAutocenterGain = atof(value); printf(" Set FFBAutocenterGain to %f\n", gFFBAutocenterGain); } + else if (strcmp(key, "FFBRumbleGain") == 0) + { + gFFBRumbleGain = atof(value); + printf(" Set FFBAutocenterGain to %f\n", gFFBRumbleGain); + } } } } From 80816d18293bea9de53b28a1f6df5201791add01 Mon Sep 17 00:00:00 2001 From: Arkhist Date: Thu, 18 Jun 2026 05:40:41 +0200 Subject: [PATCH 3/4] FFB playback (unthreaded) --- src/loader/hardware/lindbergh/forceFeedback.c | 124 ++++++++++++++++-- 1 file changed, 112 insertions(+), 12 deletions(-) diff --git a/src/loader/hardware/lindbergh/forceFeedback.c b/src/loader/hardware/lindbergh/forceFeedback.c index e7da1b6..b38d767 100644 --- a/src/loader/hardware/lindbergh/forceFeedback.c +++ b/src/loader/hardware/lindbergh/forceFeedback.c @@ -31,6 +31,25 @@ static SDL_HapticEffect friction_effect; static int friction_effect_id = -1; +static const int PLAYBACK_SPEED = 10; // ms + +typedef struct { + int direction; + float strength; +} SUD_Subpackage; + +typedef struct { + SUD_Subpackage pkg[16]; +} SUD_Package; + +static SUD_Package SUD_packages[16] = {0}; + +static int uploading_package_id = -1; +static int uploading_subpackage_id = -1; + +static SDL_HapticEffect playback_effects[16] = {0}; +static int playback_effect_ids[16] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + void sdlFfbSetStrength(float strength) { @@ -51,6 +70,16 @@ void sdlFfbInit(void) constant_effect.constant.attack_length = 0; constant_effect.constant.fade_length = 0; + for (int i = 0; i < 16; i++) { + playback_effects[i].type = SDL_HAPTIC_CONSTANT; + playback_effects[i].constant.direction.type = SDL_HAPTIC_STEERING_AXIS; + playback_effects[i].constant.direction.dir[0] = 1; + playback_effects[i].constant.attack_length = 0; + playback_effects[i].constant.fade_length = 0; + playback_effects[i].constant.length = PLAYBACK_SPEED; + playback_effects[i].constant.delay = PLAYBACK_SPEED*i; + } + memset(&friction_effect, 0, sizeof(friction_effect)); friction_effect.type = SDL_HAPTIC_DAMPER; friction_effect.condition.direction.type = SDL_HAPTIC_STEERING_AXIS; @@ -173,6 +202,45 @@ void sdlFfbConstant(int direction, float strength, uint32_t duration_ms) } } +void sdlFfbPlayback(uint8_t effect) +{ + if (!sdlJoysticks.haptics[0]) + return; + + int effect_direction = 1; + if (effect & 0b10000) { + effect ^= 0b10000; + effect_direction = -1; + } + + for (int i = 0; i < 16; i++) { + if (playback_effect_ids[i] >= 0) { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], playback_effect_ids[i]); + playback_effect_ids[i] = -1; + } + } + for (int i = 0; i < 16; i++) { + float strength = SUD_packages[effect].pkg[i].strength; + int direction = SUD_packages[effect].pkg[i].direction * effect_direction; + + playback_effects[i].type = SDL_HAPTIC_CONSTANT; // the fact that this is necessary is a sign that this should get debugged a bit. + playback_effects[i].constant.direction.type = SDL_HAPTIC_STEERING_AXIS; + playback_effects[i].constant.direction.dir[0] = 1; + playback_effects[i].constant.attack_length = 0; + playback_effects[i].constant.fade_length = 0; + playback_effects[i].constant.length = PLAYBACK_SPEED; + playback_effects[i].constant.delay = PLAYBACK_SPEED*i; + + playback_effects[i].constant.level = (strength * 0x7FFF)*direction; + //printf("\t\tPlaying effect 0x%02x 0x%02x (type: %d 0x%04x)\n", effect, i, playback_effects[i].type, playback_effects[i].constant.level); + playback_effect_ids[i] = SDL_CreateHapticEffect(sdlJoysticks.haptics[0], &playback_effects[i]); + if (playback_effect_ids[i] < 0) + log_error("\tPlayback Haptic Effect 0x%02x 0x%02x (type: %d 0x%04x) could not be created: %s\n", effect, i, playback_effects[i].type, playback_effects[i].constant.level, SDL_GetError()); + if(!SDL_RunHapticEffect(sdlJoysticks.haptics[0], playback_effect_ids[i], 1)) + log_error("\tPlayback Haptic Effect 0x%02x 0x%02x did not run: %s\n", effect, i, SDL_GetError()); + } +} + void sdlFfbFriction(float power, float coverage, uint32_t duration_ms) { if (!sdlJoysticks.haptics[0]) @@ -234,6 +302,10 @@ void sdlFfbStopEffect(void) SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], friction_effect_id); friction_effect_id = -1; } + for (int i = 0; i < 16; i++) { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], playback_effect_ids[i]); + playback_effect_ids[i] = -1; + } SDL_SetHapticAutocenter(sdlJoysticks.haptics[0], 0); } @@ -248,7 +320,7 @@ void sdlFfbShutdown(void) void sdlFfbDriveboard(const unsigned char *buffer, size_t count) { - /*if (buffer[0] != 0x80 && buffer[0] != 0x84 && buffer[0] != 0x85 && buffer[0] != 0xfd && buffer[0] != 0x86 && buffer[0] != 0x8b && buffer[0] != 0xfb){ + /*if (buffer[0] != 0x80 && buffer[0] != 0x84 && buffer[0] != 0x85 && buffer[0] != 0xfd && buffer[0] != 0x86 && buffer[0] != 0x8b){ printf("FFB driveboard: count=%zu, data:", count); for (size_t i = 0; i < count; ++i) { printf(" 0x%02x", buffer[i]); @@ -345,21 +417,49 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) } case 0xFB: { // Playback sud package - uint8_t effect = buffer[2]; - printf("Outrun FFB 0xFB effect: 0x%02x\n", effect); + //printf("Outrun FFB 0xFB effect: 0x%02x arg 0x%02x\n", effect, buffer[1]); - if (effect == 0x02) - { - sdlFfbRumble(1.0f, 1.0f, 120); + sdlFfbPlayback(effect); + + break; + } + case 0x9D: + { // Upload part 1 + if (buffer[2] > 0xf || buffer[1] > 0xf) { + log_error("Package upload 0x%02x 0x%02x 0x%02x 0x%02x got invalid IDs\n", buffer[0], buffer[1], buffer[2], buffer[3]); } - else if (effect == 0x10 || effect == 0x0B || effect == 0x04) - { - sdlFfbRumble(0.0f, 1.0f, 120); + else { + uploading_package_id = buffer[2]; + uploading_subpackage_id = buffer[1]; + //printf("Uploading package 0x%02x 0x%02x\n", buffer[2], buffer[1]); } - else if (effect == 0x00 || effect == 0x1B || effect == 0x14) - { - sdlFfbRumble(1.0f, 0.0f, 120); + break; + } + case 0x9E: + { // Upload part 2 + if (uploading_subpackage_id < 0 || uploading_package_id < 0) { + log_error("Package data 0x%02x 0x%02x 0x%02x 0x%02x cannot be uploaded yet\n", buffer[0], buffer[1], buffer[2], buffer[3]); + } + else { + uint8_t direction = buffer[1]; + uint8_t value = buffer[2]; + + float strength = 0.0f; + uint32_t duration = ffb_power_mode & FFB_PLAY_CONTINUOUS ? SDL_HAPTIC_INFINITY : RUN_ONCE_DURATION; + + if (direction == 0x00) + { // right - value from 0x7F (min) to 0x01 (max) + strength = (127.0f - (float)value) / 127.0f; + } + else if (direction == 0x01) + { // left - value goes from 0x00 (min) to 0x7F (max) + + strength = (float)value / 127.0f; + } + SUD_packages[uploading_package_id].pkg[uploading_subpackage_id].direction = ((int)direction)*2 - 1; + SUD_packages[uploading_package_id].pkg[uploading_subpackage_id].strength = strength; + //printf("Package 0x%02x 0x%02x uploaded with direction %d and strength %f\n", uploading_package_id, uploading_subpackage_id, direction, strength); } break; } From d570ce93f17ab5048df9e4756cf1266c0114fd35 Mon Sep 17 00:00:00 2001 From: Arkhist Date: Thu, 18 Jun 2026 19:18:48 +0200 Subject: [PATCH 4/4] Improved FFB playback (threaded) --- src/loader/hardware/lindbergh/baseBoard.c | 2 +- src/loader/hardware/lindbergh/forceFeedback.c | 189 +++++++++++++----- 2 files changed, 140 insertions(+), 51 deletions(-) diff --git a/src/loader/hardware/lindbergh/baseBoard.c b/src/loader/hardware/lindbergh/baseBoard.c index a379cbd..6add6c9 100644 --- a/src/loader/hardware/lindbergh/baseBoard.c +++ b/src/loader/hardware/lindbergh/baseBoard.c @@ -54,7 +54,7 @@ typedef struct FILE *sram = NULL; unsigned int sharedMemoryIndex = 0; -uint8_t sharedMemory[1024 * 32] = {0}; +uint8_t sharedMemory[1024 * 64] = {0}; int selectReply = -1; int jvsFileDescriptor = -1; diff --git a/src/loader/hardware/lindbergh/forceFeedback.c b/src/loader/hardware/lindbergh/forceFeedback.c index b38d767..5c7b4ad 100644 --- a/src/loader/hardware/lindbergh/forceFeedback.c +++ b/src/loader/hardware/lindbergh/forceFeedback.c @@ -31,7 +31,9 @@ static SDL_HapticEffect friction_effect; static int friction_effect_id = -1; -static const int PLAYBACK_SPEED = 10; // ms +static SDL_Thread *playback_thread = NULL; + +static const int PLAYBACK_SPEED = 16; // ms typedef struct { int direction; @@ -47,8 +49,20 @@ static SUD_Package SUD_packages[16] = {0}; static int uploading_package_id = -1; static int uploading_subpackage_id = -1; -static SDL_HapticEffect playback_effects[16] = {0}; -static int playback_effect_ids[16] = {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; +static SDL_HapticEffect playback_effect = {0}; +static int playback_effect_id = -1; + +// host environment +static int threading_attempted = 0; + +// shared environment +static SDL_Mutex *playback_mutex; +static SDL_Condition *playback_wakeup; +static int playback_request = -1; +static bool playback_active = false; + +// thread environment +static int playback_playing = -1; void sdlFfbSetStrength(float strength) @@ -70,21 +84,20 @@ void sdlFfbInit(void) constant_effect.constant.attack_length = 0; constant_effect.constant.fade_length = 0; - for (int i = 0; i < 16; i++) { - playback_effects[i].type = SDL_HAPTIC_CONSTANT; - playback_effects[i].constant.direction.type = SDL_HAPTIC_STEERING_AXIS; - playback_effects[i].constant.direction.dir[0] = 1; - playback_effects[i].constant.attack_length = 0; - playback_effects[i].constant.fade_length = 0; - playback_effects[i].constant.length = PLAYBACK_SPEED; - playback_effects[i].constant.delay = PLAYBACK_SPEED*i; - } + memset(&playback_effect, 0, sizeof(playback_effect)); + playback_effect.type = SDL_HAPTIC_CONSTANT; + playback_effect.constant.direction.type = SDL_HAPTIC_STEERING_AXIS; + playback_effect.constant.direction.dir[0] = 1; + playback_effect.constant.attack_length = 0; + playback_effect.constant.fade_length = 0; + playback_effect.constant.length = SDL_HAPTIC_INFINITY; memset(&friction_effect, 0, sizeof(friction_effect)); friction_effect.type = SDL_HAPTIC_DAMPER; friction_effect.condition.direction.type = SDL_HAPTIC_STEERING_AXIS; friction_effect.condition.direction.dir[0] = 1; + // SDL input mode: open haptic from already-opened joysticks for (int i = 0; i < sdlJoysticks.joysticksCount && i < MAX_JOYSTICKS; ++i) { @@ -180,14 +193,11 @@ void sdlFfbConstant(int direction, float strength, uint32_t duration_ms) return; int should_recreate = 1; - if ((ffb_power_mode & FFB_PLAY_CONTINUOUS) && constant_effect_id >= 0) { + if ((ffb_power_mode & FFB_PLAY_CONTINUOUS) && constant_effect_id >= 0) should_recreate = 0; - } if (should_recreate && constant_effect_id >= 0) - { SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], constant_effect_id); - } constant_effect.constant.length = duration_ms; constant_effect.constant.level = (strength * 0x7FFF)*direction; @@ -202,43 +212,114 @@ void sdlFfbConstant(int direction, float strength, uint32_t duration_ms) } } + +int sdlFfbPlaybackThread(void* data) +{ + uint64_t time_since_last_playback = SDL_GetTicks(); + + while(true) + { + if (playback_effect_id >= 0) { + playback_effect.constant.level = 0; + SDL_UpdateHapticEffect(sdlJoysticks.haptics[0], playback_effect_id, &playback_effect); + } + SDL_LockMutex(playback_mutex); + //printf("Waiting for a new playback...\n"); + + while (playback_request < 0 && playback_active) + SDL_WaitCondition(playback_wakeup, playback_mutex); + + if (!playback_active) { + SDL_UnlockMutex(playback_mutex); + return 0; + } + + playback_playing = playback_request; + int effect = playback_playing; + if (effect & 0b10000) { + effect = playback_playing ^ 0b10000; + } + int effect_direction = playback_playing & 0b10000 ? -1 : 1; + + //printf("Playing new effect: 0x%02x %d\n", effect, effect_direction); + playback_request = -1; + + SDL_UnlockMutex(playback_mutex); + + for (int i = 0; i < 16; i++) { + SDL_LockMutex(playback_mutex); + if (playback_request >= 0) { + SDL_UnlockMutex(playback_mutex); + //printf("Playback interrupted\n"); + playback_playing = -1; + break; + } + + int should_recreate = 1; + if ((ffb_power_mode & FFB_PLAY_CONTINUOUS) && playback_effect_id >= 0) { + should_recreate = 0; + } + + + float strength = SUD_packages[effect].pkg[i].strength; + int direction = SUD_packages[effect].pkg[i].direction * effect_direction; + //printf("\tPlaying package: 0x%02x 0x%02x %d %d, package requested: %d, interval: %d, str: %g\n", playback_playing, effect, i, should_recreate, playback_request, SDL_GetTicks() - time_since_last_playback, strength); + time_since_last_playback = SDL_GetTicks(); + + playback_effect.constant.level = (strength * 0x7FFF)*direction; + + if (should_recreate) { + playback_effect_id = SDL_CreateHapticEffect(sdlJoysticks.haptics[0], &playback_effect); + + if (playback_effect_id < 0) + log_error("\tPlayback Haptic Effect 0x%02x 0x%02x (lvl: 0x%04x) could not be created: %s\n", effect, i, playback_effect.constant.level, SDL_GetError()); + if(!SDL_RunHapticEffect(sdlJoysticks.haptics[0], playback_effect_id, 1)) + log_error("\tPlayback Haptic Effect 0x%02x 0x%02x did not run: %s\n", effect, i, SDL_GetError()); + } + else { + SDL_UpdateHapticEffect(sdlJoysticks.haptics[0], playback_effect_id, &playback_effect); + } + + SDL_WaitConditionTimeout(playback_wakeup, playback_mutex, PLAYBACK_SPEED); + + SDL_UnlockMutex(playback_mutex); + } + + SDL_LockMutex(playback_mutex); + if (playback_request == playback_playing) { + playback_request = -1; + } + playback_playing = -1; + SDL_UnlockMutex(playback_mutex); + } +} + void sdlFfbPlayback(uint8_t effect) { if (!sdlJoysticks.haptics[0]) return; - int effect_direction = 1; - if (effect & 0b10000) { - effect ^= 0b10000; - effect_direction = -1; - } - - for (int i = 0; i < 16; i++) { - if (playback_effect_ids[i] >= 0) { - SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], playback_effect_ids[i]); - playback_effect_ids[i] = -1; + if (!threading_attempted) { // if this failed once, we should not try to make another thread. + threading_attempted = 1; + playback_active = true; + playback_mutex = SDL_CreateMutex(); + playback_wakeup = SDL_CreateCondition(); + playback_thread = SDL_CreateThread(sdlFfbPlaybackThread, "FFBPlayback", NULL); + if (playback_thread == NULL) { + log_error("playback_thread could not be started: %s\n", SDL_GetError()); + playback_active = false; } + printf("Started FFB Playback thread.\n"); } - for (int i = 0; i < 16; i++) { - float strength = SUD_packages[effect].pkg[i].strength; - int direction = SUD_packages[effect].pkg[i].direction * effect_direction; - - playback_effects[i].type = SDL_HAPTIC_CONSTANT; // the fact that this is necessary is a sign that this should get debugged a bit. - playback_effects[i].constant.direction.type = SDL_HAPTIC_STEERING_AXIS; - playback_effects[i].constant.direction.dir[0] = 1; - playback_effects[i].constant.attack_length = 0; - playback_effects[i].constant.fade_length = 0; - playback_effects[i].constant.length = PLAYBACK_SPEED; - playback_effects[i].constant.delay = PLAYBACK_SPEED*i; - - playback_effects[i].constant.level = (strength * 0x7FFF)*direction; - //printf("\t\tPlaying effect 0x%02x 0x%02x (type: %d 0x%04x)\n", effect, i, playback_effects[i].type, playback_effects[i].constant.level); - playback_effect_ids[i] = SDL_CreateHapticEffect(sdlJoysticks.haptics[0], &playback_effects[i]); - if (playback_effect_ids[i] < 0) - log_error("\tPlayback Haptic Effect 0x%02x 0x%02x (type: %d 0x%04x) could not be created: %s\n", effect, i, playback_effects[i].type, playback_effects[i].constant.level, SDL_GetError()); - if(!SDL_RunHapticEffect(sdlJoysticks.haptics[0], playback_effect_ids[i], 1)) - log_error("\tPlayback Haptic Effect 0x%02x 0x%02x did not run: %s\n", effect, i, SDL_GetError()); - } + + if (!playback_active) + return; + + SDL_LockMutex(playback_mutex); + //printf("requesting FFB Playback effect 0x%02x %d\n", effect, threading_attempted); + playback_request = effect; + SDL_SignalCondition(playback_wakeup); + SDL_UnlockMutex(playback_mutex); } void sdlFfbFriction(float power, float coverage, uint32_t duration_ms) @@ -302,9 +383,9 @@ void sdlFfbStopEffect(void) SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], friction_effect_id); friction_effect_id = -1; } - for (int i = 0; i < 16; i++) { - SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], playback_effect_ids[i]); - playback_effect_ids[i] = -1; + if (playback_effect_id >= 0) { + SDL_DestroyHapticEffect(sdlJoysticks.haptics[0], playback_effect_id); + playback_effect_id = -1; } SDL_SetHapticAutocenter(sdlJoysticks.haptics[0], 0); } @@ -315,6 +396,12 @@ void sdlFfbShutdown(void) { SDL_CloseHaptic(sdlJoysticks.haptics[0]); sdlJoysticks.haptics[0] = NULL; + if (playback_active) { + SDL_LockMutex(playback_mutex); + playback_active = false; + SDL_SignalCondition(playback_wakeup); + SDL_UnlockMutex(playback_mutex); + } } } @@ -439,7 +526,7 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) case 0x9E: { // Upload part 2 if (uploading_subpackage_id < 0 || uploading_package_id < 0) { - log_error("Package data 0x%02x 0x%02x 0x%02x 0x%02x cannot be uploaded yet\n", buffer[0], buffer[1], buffer[2], buffer[3]); + log_error("FFB SUD Package data 0x%02x 0x%02x 0x%02x 0x%02x cannot be uploaded yet\n", buffer[0], buffer[1], buffer[2], buffer[3]); } else { uint8_t direction = buffer[1]; @@ -460,6 +547,8 @@ void sdlFfbDriveboard(const unsigned char *buffer, size_t count) SUD_packages[uploading_package_id].pkg[uploading_subpackage_id].direction = ((int)direction)*2 - 1; SUD_packages[uploading_package_id].pkg[uploading_subpackage_id].strength = strength; //printf("Package 0x%02x 0x%02x uploaded with direction %d and strength %f\n", uploading_package_id, uploading_subpackage_id, direction, strength); + uploading_subpackage_id = -1; + uploading_package_id = -1; } break; }