From 2251585ef026da34e054b0d17238d266b7612ff2 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 30 Sep 2024 20:23:28 +0200 Subject: [PATCH] swdc: add ffb and led emulation --- dist/swdc/segatools.ini | 26 ++- idachook/idac-dll.c | 2 +- idachook/io4.c | 2 +- idacio/di.c | 1 - swdchook/config.c | 38 ++++ swdchook/config.h | 3 + swdchook/dllmain.c | 14 ++ swdchook/ffb.c | 59 ++++++ swdchook/ffb.h | 7 + swdchook/io4.c | 35 +++- swdchook/meson.build | 2 + swdchook/swdc-dll.c | 27 +++ swdchook/swdc-dll.h | 9 + swdchook/swdchook.def | 9 + swdcio/backend.h | 5 + swdcio/config.c | 27 ++- swdcio/config.h | 6 +- swdcio/di-dev.c | 446 +++++++++++++++++++++++++++++----------- swdcio/di-dev.h | 13 +- swdcio/di.c | 33 +-- swdcio/dllmain.c | 80 ++++++- swdcio/swdcio.def | 9 + swdcio/swdcio.h | 118 ++++++++++- swdcio/xi.c | 47 ++++- 24 files changed, 872 insertions(+), 146 deletions(-) create mode 100644 swdchook/ffb.c create mode 100644 swdchook/ffb.h diff --git a/dist/swdc/segatools.ini b/dist/swdc/segatools.ini index 72ea69d..b6b15b8 100644 --- a/dist/swdc/segatools.ini +++ b/dist/swdc/segatools.ini @@ -65,6 +65,11 @@ enable=1 ; allow you to start a game in freeplay mode. freeplay=0 +[ffb] +; Enable force feedback (838-15069) board emulation. This is required for +; both DirectInput and XInput steering wheel effects. +enable=1 + ; ----------------------------------------------------------------------------- ; Custom IO settings ; ----------------------------------------------------------------------------- @@ -181,6 +186,21 @@ reverseAccelAxis=0 reverseBrakeAxis=0 ; Force feedback settings. -; Strength of the force feedback spring effect in percent. Possible values -; are 0-100. -centerSpringStrength=30 +; Only works when FFB board emulation is enabled! +; +; It is recommended to change the strength inside the Game Test Mode! +; +; These settings are only used when using DirectInput for the wheel. +; The values are in the range 0%-100%, where 0 disables the effect and +; 100 is the maximum. + +; Constant force strength, used for centering spring effect. +constantForceStrength=100 +; Damper strength, used for steering wheel damper effect. +damperStrength=100 + +; Rumble strength, used for road surface effects. +; WARNING: THIS WILL CRASH ON FANATEC (maybe even more) WHEELS! +rumbleStrength=100 +; Rumble duration factor from ms to µs, used to scale the duration of the rumble effect. +rumbleDuration=1000 diff --git a/idachook/idac-dll.c b/idachook/idac-dll.c index 5b09d91..224bca6 100644 --- a/idachook/idac-dll.c +++ b/idachook/idac-dll.c @@ -51,7 +51,7 @@ const struct dll_bind_sym idac_dll_syms[] = { }, { .sym = "idac_io_ffb_damper", .off = offsetof(struct idac_dll, ffb_damper), - }, + } }; struct idac_dll idac_dll; diff --git a/idachook/io4.c b/idachook/io4.c index 5340f5a..c13e7a5 100644 --- a/idachook/io4.c +++ b/idachook/io4.c @@ -15,7 +15,7 @@ static HRESULT idac_io4_write_gpio(uint8_t* payload, size_t len); static uint16_t coins; static const struct io4_ops idac_io4_ops = { - .poll = idac_io4_poll, + .poll = idac_io4_poll, .write_gpio = idac_io4_write_gpio }; diff --git a/idacio/di.c b/idacio/di.c index fb50ed1..5310774 100644 --- a/idacio/di.c +++ b/idacio/di.c @@ -79,7 +79,6 @@ static uint8_t idac_di_gear[6]; static bool idac_di_use_pedals; static bool idac_di_reverse_brake_axis; static bool idac_di_reverse_accel_axis; -static uint16_t idac_di_center_spring_strength; HRESULT idac_di_init( const struct idac_di_config *cfg, diff --git a/swdchook/config.c b/swdchook/config.c index 4513fd8..62115fa 100644 --- a/swdchook/config.c +++ b/swdchook/config.c @@ -13,6 +13,43 @@ #include "platform/config.h" #include "platform/platform.h" + +void led15070_config_load(struct led15070_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + wchar_t tmpstr[16]; + + cfg->enable = GetPrivateProfileIntW(L"led15070", L"enable", 1, filename); + cfg->port_no = GetPrivateProfileIntW(L"led15070", L"portNo", 0, filename); + cfg->fw_ver = GetPrivateProfileIntW(L"led15070", L"fwVer", 0x90, filename); + /* TODO: Unknown, no firmware file available */ + cfg->fw_sum = GetPrivateProfileIntW(L"led15070", L"fwSum", 0xdead, filename); + + GetPrivateProfileStringW( + L"led15070", + L"boardNumber", + L"15070-02", + tmpstr, + _countof(tmpstr), + filename); + + size_t n = wcstombs(cfg->board_number, tmpstr, sizeof(cfg->board_number)); + for (int i = n; i < sizeof(cfg->board_number); i++) + { + cfg->board_number[i] = ' '; + } + + GetPrivateProfileStringW( + L"led15070", + L"eepromPath", + L"DEVICE", + cfg->eeprom_path, + _countof(cfg->eeprom_path), + filename); +} + void swdc_dll_config_load( struct swdc_dll_config *cfg, const wchar_t *filename) @@ -43,6 +80,7 @@ void swdc_hook_config_load( dvd_config_load(&cfg->dvd, filename); io4_config_load(&cfg->io4, filename); vfd_config_load(&cfg->vfd, filename); + led15070_config_load(&cfg->led15070, filename); } void zinput_config_load(struct zinput_config *cfg, const wchar_t *filename) diff --git a/swdchook/config.h b/swdchook/config.h index 587ab66..976285a 100644 --- a/swdchook/config.h +++ b/swdchook/config.h @@ -4,6 +4,7 @@ #include #include "board/config.h" +#include "board/led15070.h" #include "hooklib/dvd.h" @@ -17,7 +18,9 @@ struct swdc_hook_config { struct aime_config aime; struct dvd_config dvd; struct io4_config io4; + struct ffb_config ffb; struct vfd_config vfd; + struct led15070_config led15070; struct swdc_dll_config dll; struct zinput_config zinput; }; diff --git a/swdchook/dllmain.c b/swdchook/dllmain.c index 94d68ea..f9c6081 100644 --- a/swdchook/dllmain.c +++ b/swdchook/dllmain.c @@ -31,6 +31,7 @@ #include "swdchook/config.h" #include "swdchook/swdc-dll.h" #include "swdchook/io4.h" +#include "swdchook/ffb.h" #include "platform/platform.h" @@ -91,6 +92,19 @@ static DWORD CALLBACK swdc_pre_startup(void) goto fail; } + hr = swdc_ffb_hook_init(&swdc_hook_cfg.ffb, 1); + + if (FAILED(hr)) { + goto fail; + } + + hr = led15070_hook_init(&swdc_hook_cfg.led15070, swdc_dll.led_init, + swdc_dll.led_set_fet_output, NULL, swdc_dll.led_gs_update, 2, 1); + + if (FAILED(hr)) { + goto fail; + } + /* Hook external DLL APIs */ zinput_hook_init(&swdc_hook_cfg.zinput); diff --git a/swdchook/ffb.c b/swdchook/ffb.c new file mode 100644 index 0000000..07c6dea --- /dev/null +++ b/swdchook/ffb.c @@ -0,0 +1,59 @@ +#include + +#include +#include +#include +#include + +#include "board/ffb.h" + +#include "swdchook/swdc-dll.h" + +#include "util/dprintf.h" + +static void swdc_ffb_toggle(bool active); +static void swdc_ffb_constant_force(uint8_t direction, uint8_t force); +static void swdc_ffb_rumble(uint8_t force, uint8_t period); +static void swdc_ffb_damper(uint8_t force); + +static const struct ffb_ops swdc_ffb_ops = { + .toggle = swdc_ffb_toggle, + .constant_force = swdc_ffb_constant_force, + .rumble = swdc_ffb_rumble, + .damper = swdc_ffb_damper +}; + +HRESULT swdc_ffb_hook_init(const struct ffb_config *cfg, unsigned int port_no) +{ + HRESULT hr; + + assert(swdc_dll.init != NULL); + + hr = ffb_hook_init(cfg, &swdc_ffb_ops, port_no); + + if (FAILED(hr)) { + return hr; + } + + return swdc_dll.ffb_init(); +} + +static void swdc_ffb_toggle(bool active) +{ + swdc_dll.ffb_toggle(active); +} + +static void swdc_ffb_constant_force(uint8_t direction, uint8_t force) +{ + swdc_dll.ffb_constant_force(direction, force); +} + +static void swdc_ffb_rumble(uint8_t force, uint8_t period) +{ + swdc_dll.ffb_rumble(force, period); +} + +static void swdc_ffb_damper(uint8_t force) +{ + swdc_dll.ffb_damper(force); +} diff --git a/swdchook/ffb.h b/swdchook/ffb.h new file mode 100644 index 0000000..53a47a4 --- /dev/null +++ b/swdchook/ffb.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "board/ffb.h" + +HRESULT swdc_ffb_hook_init(const struct ffb_config *cfg, unsigned int port_no); diff --git a/swdchook/io4.c b/swdchook/io4.c index d9f2198..5221f63 100644 --- a/swdchook/io4.c +++ b/swdchook/io4.c @@ -15,10 +15,12 @@ static HRESULT init_mmf(void); static void swdc_set_gamebtns(uint16_t value); static HRESULT swdc_io4_poll(void *ctx, struct io4_state *state); +static HRESULT swdc_io4_write_gpio(uint8_t* payload, size_t len); static uint16_t coins; static const struct io4_ops swdc_io4_ops = { - .poll = swdc_io4_poll, + .poll = swdc_io4_poll, + .write_gpio = swdc_io4_write_gpio }; HRESULT swdc_io4_hook_init(const struct io4_config *cfg) { @@ -172,3 +174,34 @@ static HRESULT swdc_io4_poll(void *ctx, struct io4_state *state) { return S_OK; } + +static HRESULT swdc_io4_write_gpio(uint8_t* payload, size_t len) +{ + // Just fast fail if there aren't enough bytes in the payload + if (len < 3) + return S_OK; + + // This command is used for lights in SWDC, but it only contains button lights, + // and only in the first 3 bytes of the payload; everything else is padding to + // make the payload 62 bytes. The rest of the cabinet lights and the side button + // lights are handled separately, by the 15070 lights controller. + uint32_t lights_data = (uint32_t) ((uint8_t)(payload[0]) << 24 | + (uint8_t)(payload[1]) << 16 | + (uint8_t)(payload[2]) << 8); + + // Since Sega uses an odd ordering for the first part of the bitfield, + // let's normalize the data and just send over bytes for the receiver + // to interpret as ON/OFF values. + uint8_t rgb_out[6] = { + lights_data & SWDC_IO_LED_START ? 0xFF : 0x00, + lights_data & SWDC_IO_LED_VIEW_CHANGE ? 0xFF : 0x00, + lights_data & SWDC_IO_LED_UP ? 0xFF : 0x00, + lights_data & SWDC_IO_LED_DOWN ? 0xFF : 0x00, + lights_data & SWDC_IO_LED_RIGHT ? 0xFF : 0x00, + lights_data & SWDC_IO_LED_LEFT ? 0xFF : 0x00, + }; + + swdc_dll.led_set_leds(rgb_out); + + return S_OK; +} diff --git a/swdchook/meson.build b/swdchook/meson.build index c312ed4..786c936 100644 --- a/swdchook/meson.build +++ b/swdchook/meson.build @@ -28,5 +28,7 @@ shared_library( 'io4.h', 'zinput.c', 'zinput.h', + 'ffb.c', + 'ffb.h' ], ) diff --git a/swdchook/swdc-dll.c b/swdchook/swdc-dll.c index 80d4526..0897b73 100644 --- a/swdchook/swdc-dll.c +++ b/swdchook/swdc-dll.c @@ -21,6 +21,33 @@ const struct dll_bind_sym swdc_dll_syms[] = { }, { .sym = "swdc_io_get_analogs", .off = offsetof(struct swdc_dll, get_analogs), + }, { + .sym = "swdc_io_led_init", + .off = offsetof(struct swdc_dll, led_init), + }, { + .sym = "swdc_io_led_set_fet_output", + .off = offsetof(struct swdc_dll, led_set_fet_output), + }, { + .sym = "swdc_io_led_gs_update", + .off = offsetof(struct swdc_dll, led_gs_update), + }, { + .sym = "swdc_io_led_set_leds", + .off = offsetof(struct swdc_dll, led_set_leds), + }, { + .sym = "swdc_io_ffb_init", + .off = offsetof(struct swdc_dll, ffb_init), + }, { + .sym = "swdc_io_ffb_toggle", + .off = offsetof(struct swdc_dll, ffb_toggle), + }, { + .sym = "swdc_io_ffb_constant_force", + .off = offsetof(struct swdc_dll, ffb_constant_force), + }, { + .sym = "swdc_io_ffb_rumble", + .off = offsetof(struct swdc_dll, ffb_rumble), + }, { + .sym = "swdc_io_ffb_damper", + .off = offsetof(struct swdc_dll, ffb_damper), } }; diff --git a/swdchook/swdc-dll.h b/swdchook/swdc-dll.h index 87781c8..1b81e26 100644 --- a/swdchook/swdc-dll.h +++ b/swdchook/swdc-dll.h @@ -10,6 +10,15 @@ struct swdc_dll { void (*get_opbtns)(uint8_t *opbtn); void (*get_gamebtns)(uint16_t *gamebtn); void (*get_analogs)(struct swdc_io_analog_state *out); + HRESULT (*led_init)(void); + void (*led_set_fet_output)(const uint8_t *rgb); + void (*led_gs_update)(const uint8_t *rgb); + void (*led_set_leds)(const uint8_t *rgb); + HRESULT (*ffb_init)(void); + void (*ffb_toggle)(bool active); + void (*ffb_constant_force)(uint8_t direction, uint8_t force); + void (*ffb_rumble)(uint8_t period, uint8_t force); + void (*ffb_damper)(uint8_t force); }; struct swdc_dll_config { diff --git a/swdchook/swdchook.def b/swdchook/swdchook.def index 160cb7d..ad4f93e 100644 --- a/swdchook/swdchook.def +++ b/swdchook/swdchook.def @@ -16,3 +16,12 @@ EXPORTS swdc_io_get_opbtns swdc_io_get_gamebtns swdc_io_get_analogs + swdc_io_led_init + swdc_io_led_set_fet_output + swdc_io_led_gs_update + swdc_io_led_set_leds + swdc_io_ffb_init + swdc_io_ffb_toggle + swdc_io_ffb_constant_force + swdc_io_ffb_rumble + swdc_io_ffb_damper diff --git a/swdcio/backend.h b/swdcio/backend.h index d30645b..85517a4 100644 --- a/swdcio/backend.h +++ b/swdcio/backend.h @@ -8,4 +8,9 @@ struct swdc_io_backend { void (*get_opbtns)(uint8_t *opbtn); void (*get_gamebtns)(uint16_t *gamebtn); void (*get_analogs)(struct swdc_io_analog_state *state); + HRESULT (*ffb_init)(void); + void (*ffb_toggle)(bool active); + void (*ffb_constant_force)(uint8_t direction, uint8_t force); + void (*ffb_rumble)(uint8_t period, uint8_t force); + void (*ffb_damper)(uint8_t force); }; diff --git a/swdcio/config.c b/swdcio/config.c index 131b403..4f095ce 100644 --- a/swdcio/config.c +++ b/swdcio/config.c @@ -69,12 +69,29 @@ void swdc_di_config_load(struct swdc_di_config *cfg, const wchar_t *filename) filename); // FFB configuration + cfg->ffb_constant_force_strength = GetPrivateProfileIntW( + L"dinput", + L"constantForceStrength", + 100, + filename); - cfg->center_spring_strength = GetPrivateProfileIntW( - L"dinput", - L"centerSpringStrength", - 30, - filename); + cfg->ffb_rumble_strength = GetPrivateProfileIntW( + L"dinput", + L"rumbleStrength", + 100, + filename); + + cfg->ffb_damper_strength = GetPrivateProfileIntW( + L"dinput", + L"damperStrength", + 100, + filename); + + cfg->ffb_rumble_duration = GetPrivateProfileIntW( + L"dinput", + L"rumbleDuration", + 1000, + filename); } void swdc_xi_config_load(struct swdc_xi_config *cfg, const wchar_t *filename) diff --git a/swdcio/config.h b/swdcio/config.h index ba3240c..9a630a2 100644 --- a/swdcio/config.h +++ b/swdcio/config.h @@ -21,7 +21,11 @@ struct swdc_di_config { bool reverse_accel_axis; // FFB configuration - uint16_t center_spring_strength; + uint8_t ffb_constant_force_strength; + uint8_t ffb_rumble_strength; + uint8_t ffb_damper_strength; + + uint32_t ffb_rumble_duration; }; struct swdc_xi_config { diff --git a/swdcio/di-dev.c b/swdcio/di-dev.c index 254596a..85db2fb 100644 --- a/swdcio/di-dev.c +++ b/swdcio/di-dev.c @@ -1,134 +1,39 @@ #include #include - +#include #include #include "swdcio/di-dev.h" #include "util/dprintf.h" -HRESULT swdc_di_dev_start(IDirectInputDevice8W *dev, HWND wnd) +const struct swdc_di_config *swdc_di_cfg; +static HWND swdc_di_wnd; +static IDirectInputDevice8W *swdc_di_dev; + +/* Individual DI Effects */ +static IDirectInputEffect *swdc_di_fx; +static IDirectInputEffect *swdc_di_fx_rumble; +static IDirectInputEffect *swdc_di_fx_damper; + +/* Max FFB Board value is 127 */ +static const double swdc_di_ffb_scale = 127.0; + +HRESULT swdc_di_dev_init( + const struct swdc_di_config *cfg, + IDirectInputDevice8W *dev, + HWND wnd) { HRESULT hr; assert(dev != NULL); assert(wnd != NULL); - hr = IDirectInputDevice8_SetCooperativeLevel( - dev, - wnd, - DISCL_BACKGROUND | DISCL_EXCLUSIVE); + swdc_di_cfg = cfg; + swdc_di_dev = dev; + swdc_di_wnd = wnd; - if (FAILED(hr)) { - dprintf("DirectInput: SetCooperativeLevel failed: %08x\n", (int) hr); - - return hr; - } - - hr = IDirectInputDevice8_SetDataFormat(dev, &c_dfDIJoystick); - - if (FAILED(hr)) { - dprintf("DirectInput: SetDataFormat failed: %08x\n", (int) hr); - - return hr; - } - - hr = IDirectInputDevice8_Acquire(dev); - - if (FAILED(hr)) { - dprintf("DirectInput: Acquire failed: %08x\n", (int) hr); - - return hr; - } - - return hr; -} - -void swdc_di_dev_start_fx( - IDirectInputDevice8W *dev, IDirectInputEffect **out, uint16_t strength) -{ - /* Set up force-feedback on devices that support it. This is just a stub - for the time being, since we don't yet know how the serial port force - feedback protocol works. - - I'm currently developing with an Xbox One Thrustmaster TMX wheel, if - we don't perform at least some perfunctory FFB initialization of this - nature (or indeed if no DirectInput application is running) then the - wheel exhibits considerable resistance, similar to that of a stationary - car. Changing cf.lMagnitude to a nonzero value does cause the wheel to - continuously turn in the given direction with the given force as one - would expect (max magnitude per DirectInput docs is +/- 10000). - - Failure here is non-fatal, we log any errors and move on. - - https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee416353(v=vs.85) - */ - - IDirectInputEffect *obj; - DWORD axis; - LONG direction; - DIEFFECT fx; - DICONDITION cond; - HRESULT hr; - - assert(dev != NULL); - assert(out != NULL); - - *out = NULL; - - dprintf("DirectInput: Starting force feedback (may take a sec)\n"); - - // Auto-centering effect - axis = DIJOFS_X; - direction = 0; - - memset(&cond, 0, sizeof(cond)); - cond.lOffset = 0; - cond.lPositiveCoefficient = strength; - cond.lNegativeCoefficient = strength; - cond.dwPositiveSaturation = strength; // For FG920? - cond.dwNegativeSaturation = strength; // For FG920? - cond.lDeadBand = 0; - - memset(&fx, 0, sizeof(fx)); - fx.dwSize = sizeof(fx); - fx.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS; - fx.dwDuration = INFINITE; - fx.dwGain = DI_FFNOMINALMAX; - fx.dwTriggerButton = DIEB_NOTRIGGER; - fx.dwTriggerRepeatInterval = INFINITE; - fx.cAxes = 1; - fx.rgdwAxes = &axis; - fx.rglDirection = &direction; - fx.cbTypeSpecificParams = sizeof(cond); - fx.lpvTypeSpecificParams = &cond; - - hr = IDirectInputDevice8_CreateEffect( - dev, - &GUID_Spring, - &fx, - &obj, - NULL); - - if (FAILED(hr)) { - dprintf("DirectInput: Centering spring force feedback unavailable: %08x\n", - (int) hr); - return; - } - - hr = IDirectInputEffect_Start(obj, INFINITE, 0); - - if (FAILED(hr)) { - IDirectInputEffect_Release(obj); - dprintf("DirectInput: Centering spring force feedback start failed: %08x\n", - (int) hr); - return; - } - - *out = obj; - - dprintf("DirectInput: Centering spring effects initialized with strength %d%%\n", - strength / 100); + return S_OK; } HRESULT swdc_di_dev_poll( @@ -167,3 +72,312 @@ HRESULT swdc_di_dev_poll( return hr; } + +HRESULT swdc_di_dev_start(IDirectInputDevice8W *dev, HWND wnd) { + HRESULT hr; + + assert(dev != NULL); + assert(wnd != NULL); + + hr = IDirectInputDevice8_SetCooperativeLevel( + dev, + wnd, + DISCL_BACKGROUND | DISCL_EXCLUSIVE); + + if (FAILED(hr)) { + dprintf("DirectInput: SetCooperativeLevel failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInputDevice8_SetDataFormat(dev, &c_dfDIJoystick); + + if (FAILED(hr)) { + dprintf("DirectInput: SetDataFormat failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInputDevice8_Acquire(dev); + + if (FAILED(hr)) { + dprintf("DirectInput: Acquire failed: %08x\n", (int) hr); + + return hr; + } + + return hr; +} + +HRESULT swdc_di_ffb_init(void) +{ + HRESULT hr; + + hr = swdc_di_dev_start(swdc_di_dev, swdc_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + return S_OK; +} + +void swdc_di_ffb_toggle(bool active) +{ + if (active) { + return; + } + + /* Stop and release all effects */ + /* I never programmed DirectInput Effects, so this might be bad practice. */ + if (swdc_di_fx != NULL) { + IDirectInputEffect_Stop(swdc_di_fx); + IDirectInputEffect_Release(swdc_di_fx); + swdc_di_fx = NULL; + } + + if (swdc_di_fx_rumble != NULL) { + IDirectInputEffect_Stop(swdc_di_fx_rumble); + IDirectInputEffect_Release(swdc_di_fx_rumble); + swdc_di_fx_rumble = NULL; + } + + if (swdc_di_fx_damper != NULL) { + IDirectInputEffect_Stop(swdc_di_fx_damper); + IDirectInputEffect_Release(swdc_di_fx_damper); + swdc_di_fx_damper = NULL; + } +} + +void swdc_di_ffb_constant_force(uint8_t direction_ffb, uint8_t force) +{ + /* DI expects a magnitude in the range of -10.000 to 10.000 */ + uint16_t ffb_strength = swdc_di_cfg->ffb_constant_force_strength * 100; + if (ffb_strength == 0) { + return; + } + + DWORD axis; + LONG direction; + DIEFFECT fx; + DICONSTANTFORCE cf; + HRESULT hr; + + /* Direction 0: move to the right, 1: move to the left */ + LONG magnitude = (LONG)(((double)force / swdc_di_ffb_scale) * ffb_strength); + cf.lMagnitude = (direction_ffb == 0) ? -magnitude : magnitude; + + axis = DIJOFS_X; + /* Irrelevant as magnitude descripbes the direction */ + direction = 0; + + memset(&fx, 0, sizeof(fx)); + fx.dwSize = sizeof(fx); + fx.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS; + fx.dwDuration = INFINITE; + fx.dwGain = DI_FFNOMINALMAX; + fx.dwTriggerButton = DIEB_NOTRIGGER; + fx.dwTriggerRepeatInterval = INFINITE; + fx.cAxes = 1; + fx.rgdwAxes = &axis; + fx.rglDirection = &direction; + fx.cbTypeSpecificParams = sizeof(cf); + fx.lpvTypeSpecificParams = &cf; + + if (swdc_di_fx != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(swdc_di_fx, &fx, DIEP_TYPESPECIFICPARAMS); + + if (SUCCEEDED(hr)) { + return; + } else { + dprintf("DirectInput: Failed to update constant force feedback, recreating effect: %08x\n", (int)hr); + // Stop and release the current effect if updating fails + IDirectInputEffect_Stop(swdc_di_fx); + IDirectInputEffect_Release(swdc_di_fx); + swdc_di_fx = NULL; + } + } + + // Create a new constant force effect + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + swdc_di_dev, + &GUID_ConstantForce, + &fx, + &obj, + NULL); + + if (FAILED(hr)) { + dprintf("DirectInput: Constant force feedback creation failed: %08x\n", (int) hr); + return; + } + + hr = IDirectInputEffect_Start(obj, INFINITE, 0); + if (FAILED(hr)) { + dprintf("DirectInput: Constant force feedback start failed: %08x\n", (int) hr); + IDirectInputEffect_Release(obj); + return; + } + + swdc_di_fx = obj; +} + +void swdc_di_ffb_rumble(uint8_t force, uint8_t period) +{ + /* DI expects a magnitude in the range of -10.000 to 10.000 */ + uint16_t ffb_strength = swdc_di_cfg->ffb_rumble_strength * 100; + if (ffb_strength == 0) { + return; + } + + uint32_t ffb_duration = swdc_di_cfg->ffb_rumble_duration; + + DWORD axis; + LONG direction; + DIEFFECT fx; + DIPERIODIC pe; + HRESULT hr; + + /* Duration in microseconds, + Might be totally wrong as especially on FANATEC wheels as this code will + crash the game. TODO: Figure out why this effect will crash on FANATEC! */ + DWORD duration = (DWORD)((double)force * ffb_duration); + + memset(&pe, 0, sizeof(pe)); + pe.dwMagnitude = (DWORD)(((double)force / swdc_di_ffb_scale) * ffb_strength); + pe.lOffset = 0; + pe.dwPhase = 0; + pe.dwPeriod = duration; + + axis = DIJOFS_X; + direction = 0; + + memset(&fx, 0, sizeof(fx)); + fx.dwSize = sizeof(fx); + fx.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS; + fx.dwDuration = duration; + fx.dwGain = DI_FFNOMINALMAX; + fx.dwTriggerButton = DIEB_NOTRIGGER; + fx.dwTriggerRepeatInterval = INFINITE; + fx.cAxes = 1; + fx.rgdwAxes = &axis; + fx.rglDirection = &direction; + fx.cbTypeSpecificParams = sizeof(pe); + fx.lpvTypeSpecificParams = &pe; + + if (swdc_di_fx_rumble != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(swdc_di_fx_rumble, &fx, DIEP_TYPESPECIFICPARAMS); + + if (SUCCEEDED(hr)) { + return; + } else { + dprintf("DirectInput: Failed to update periodic force feedback, recreating effect: %08x\n", (int)hr); + // Stop and release the current effect if updating fails + IDirectInputEffect_Stop(swdc_di_fx_rumble); + IDirectInputEffect_Release(swdc_di_fx_rumble); + swdc_di_fx_rumble = NULL; + } + } + + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + swdc_di_dev, + &GUID_Sine, + &fx, + &obj, + NULL); + + if (FAILED(hr)) { + dprintf("DirectInput: Periodic force feedback creation failed: %08x\n", (int) hr); + return; + } + + hr = IDirectInputEffect_Start(obj, INFINITE, 0); + if (FAILED(hr)) { + dprintf("DirectInput: Periodic force feedback start failed: %08x\n", (int) hr); + IDirectInputEffect_Release(obj); + return; + } + + swdc_di_fx_rumble = obj; +} + +void swdc_di_ffb_damper(uint8_t force) +{ + /* DI expects a coefficient in the range of -10.000 to 10.000 */ + uint16_t ffb_strength = swdc_di_cfg->ffb_damper_strength * 100; + if (ffb_strength == 0) { + return; + } + + DWORD axis; + LONG direction; + DIEFFECT fx; + DICONDITION cond; + HRESULT hr; + + memset(&cond, 0, sizeof(cond)); + cond.lOffset = 0; + cond.lPositiveCoefficient = (LONG)(((double)force / swdc_di_ffb_scale) * ffb_strength); + cond.lNegativeCoefficient = (LONG)(((double)force / swdc_di_ffb_scale) * ffb_strength); + /* Not sure on this one */ + cond.dwPositiveSaturation = DI_FFNOMINALMAX; + cond.dwNegativeSaturation = DI_FFNOMINALMAX; + cond.lDeadBand = 0; + + axis = DIJOFS_X; + direction = 0; + + memset(&fx, 0, sizeof(fx)); + fx.dwSize = sizeof(fx); + fx.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS; + fx.dwDuration = INFINITE; + fx.dwGain = DI_FFNOMINALMAX; + fx.dwTriggerButton = DIEB_NOTRIGGER; + fx.dwTriggerRepeatInterval = INFINITE; + fx.cAxes = 1; + fx.rgdwAxes = &axis; + fx.rglDirection = &direction; + fx.cbTypeSpecificParams = sizeof(cond); + fx.lpvTypeSpecificParams = &cond; + + if (swdc_di_fx_damper != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(swdc_di_fx_damper, &fx, DIEP_TYPESPECIFICPARAMS); + + if (SUCCEEDED(hr)) { + return; + } else { + dprintf("DirectInput: Failed to update damper force feedback, recreating effect: %08x\n", (int)hr); + // Stop and release the current effect if updating fails + IDirectInputEffect_Stop(swdc_di_fx_damper); + IDirectInputEffect_Release(swdc_di_fx_damper); + swdc_di_fx_damper = NULL; + } + } + + // Create a new damper force effect + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + swdc_di_dev, + &GUID_Damper, + &fx, + &obj, + NULL); + + if (FAILED(hr)) { + dprintf("DirectInput: Damper force feedback creation failed: %08x\n", (int) hr); + return; + } + + hr = IDirectInputEffect_Start(obj, INFINITE, 0); + if (FAILED(hr)) { + dprintf("DirectInput: Damper force feedback start failed: %08x\n", (int) hr); + IDirectInputEffect_Release(obj); + return; + } + + swdc_di_fx_damper = obj; +} diff --git a/swdcio/di-dev.h b/swdcio/di-dev.h index 06ac036..d28afdb 100644 --- a/swdcio/di-dev.h +++ b/swdcio/di-dev.h @@ -5,15 +5,26 @@ #include +#include "swdcio/config.h" + union swdc_di_state { DIJOYSTATE st; uint8_t bytes[sizeof(DIJOYSTATE)]; }; +HRESULT swdc_di_dev_init( + const struct swdc_di_config *cfg, + IDirectInputDevice8W *dev, + HWND wnd); + HRESULT swdc_di_dev_start(IDirectInputDevice8W *dev, HWND wnd); -void swdc_di_dev_start_fx(IDirectInputDevice8W *dev, IDirectInputEffect **out, uint16_t strength); HRESULT swdc_di_dev_poll( IDirectInputDevice8W *dev, HWND wnd, union swdc_di_state *out); +HRESULT swdc_di_ffb_init(void); +void swdc_di_ffb_toggle(bool active); +void swdc_di_ffb_constant_force(uint8_t direction, uint8_t force); +void swdc_di_ffb_rumble(uint8_t force, uint8_t period); +void swdc_di_ffb_damper(uint8_t force); diff --git a/swdcio/di.c b/swdcio/di.c index 7639a4e..db952a0 100644 --- a/swdcio/di.c +++ b/swdcio/di.c @@ -45,8 +45,13 @@ static const struct swdc_di_axis swdc_di_axes[] = { }; static const struct swdc_io_backend swdc_di_backend = { - .get_gamebtns = swdc_di_get_buttons, - .get_analogs = swdc_di_get_analogs, + .get_gamebtns = swdc_di_get_buttons, + .get_analogs = swdc_di_get_analogs, + .ffb_init = swdc_di_ffb_init, + .ffb_toggle = swdc_di_ffb_toggle, + .ffb_constant_force = swdc_di_ffb_constant_force, + .ffb_rumble = swdc_di_ffb_rumble, + .ffb_damper = swdc_di_ffb_damper }; static HWND swdc_di_wnd; @@ -67,7 +72,6 @@ static uint8_t swdc_di_wheel_yellow; static bool swdc_di_use_pedals; static bool swdc_di_reverse_brake_axis; static bool swdc_di_reverse_accel_axis; -static uint16_t swdc_di_center_spring_strength; HRESULT swdc_di_init( const struct swdc_di_config *cfg, @@ -160,16 +164,12 @@ HRESULT swdc_di_init( return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } - hr = swdc_di_dev_start(swdc_di_dev, swdc_di_wnd); + hr = swdc_di_dev_init(cfg, swdc_di_dev, swdc_di_wnd); if (FAILED(hr)) { return hr; } - // Convert the strength from 0-100 to 0-10000 for DirectInput - swdc_di_dev_start_fx(swdc_di_dev, &swdc_di_fx, - swdc_di_center_spring_strength * 100); - if (cfg->pedals_name[0] != L'\0') { hr = IDirectInput8_EnumDevices( swdc_di_api, @@ -320,15 +320,24 @@ static HRESULT swdc_di_config_apply(const struct swdc_di_config *cfg) swdc_di_reverse_brake_axis = cfg->reverse_brake_axis; swdc_di_reverse_accel_axis = cfg->reverse_accel_axis; - // FFB configuration + /* FFB configuration */ + if (cfg->ffb_constant_force_strength < 0 || cfg->ffb_constant_force_strength > 100) { + dprintf("Wheel: Invalid constant force strength: %i\n", cfg->ffb_constant_force_strength); - if (cfg->center_spring_strength < 0 || cfg->center_spring_strength > 100) { - dprintf("Wheel: Invalid center spring strength: %i\n", cfg->center_spring_strength); + return E_INVALIDARG; + } + + if (cfg->ffb_rumble_strength < 0 || cfg->ffb_rumble_strength > 100) { + dprintf("Wheel: Invalid rumble strength: %i\n", cfg->ffb_rumble_strength); return E_INVALIDARG; } - swdc_di_center_spring_strength = cfg->center_spring_strength; + if (cfg->ffb_damper_strength < 0 || cfg->ffb_damper_strength > 100) { + dprintf("Wheel: Invalid damper strength: %i\n", cfg->ffb_damper_strength); + + return E_INVALIDARG; + } return S_OK; } diff --git a/swdcio/dllmain.c b/swdcio/dllmain.c index ead174c..c2c5078 100644 --- a/swdcio/dllmain.c +++ b/swdcio/dllmain.c @@ -19,7 +19,7 @@ static bool swdc_io_coin; uint16_t swdc_io_get_api_version(void) { - return 0x0100; + return 0x0102; } HRESULT swdc_io_init(void) @@ -62,6 +62,8 @@ void swdc_io_get_opbtns(uint8_t *opbtn_out) opbtn = 0; + /* Common operator buttons, not backend-specific */ + if (GetAsyncKeyState(swdc_io_cfg.vk_test) & 0x8000) { opbtn |= SWDC_IO_OPBTN_TEST; } @@ -110,3 +112,79 @@ void swdc_io_get_analogs(struct swdc_io_analog_state *out) out->accel = tmp.accel; out->brake = tmp.brake; } + +HRESULT swdc_io_led_init(void) +{ + return S_OK; +} + +void swdc_io_led_set_fet_output(const uint8_t *rgb) +{ +#if 0 + dprintf("SWDC LED: LEFT SEAT LED: %02X\n", rgb[0]); + dprintf("SWDC LED: RIGHT SEAT LED: %02X\n", rgb[1]); +#endif + + return; +} + +void swdc_io_led_gs_update(const uint8_t *rgb) +{ +#if 0 + for (int i = 0; i < 9; i++) { + dprintf("SWDC LED: LED %d: %02X %02X %02X Speed: %02X\n", + i, rgb[i * 4], rgb[i * 4 + 1], rgb[i * 4 + 2], rgb[i * 4 + 3]); + } +#endif + + return; +} + +void swdc_io_led_set_leds(const uint8_t *rgb) +{ +#if 0 + dprintf("SWDC LED: START: %02X\n", rgb[0]); + dprintf("SWDC LED: VIEW CHANGE: %02X\n", rgb[1]); + dprintf("SWDC LED: UP: %02X\n", rgb[2]); + dprintf("SWDC LED: DOWN: %02X\n", rgb[3]); + dprintf("SWDC LED: RIGHT: %02X\n", rgb[4]); + dprintf("SWDC LED: LEFT: %02X\n", rgb[5]); +#endif + + return; +} + +HRESULT swdc_io_ffb_init(void) +{ + assert(swdc_io_backend != NULL); + + return swdc_io_backend->ffb_init(); +} + +void swdc_io_ffb_toggle(bool active) +{ + assert(swdc_io_backend != NULL); + + swdc_io_backend->ffb_toggle(active); +} + +void swdc_io_ffb_constant_force(uint8_t direction, uint8_t force) +{ + assert(swdc_io_backend != NULL); + + swdc_io_backend->ffb_constant_force(direction, force); +} + +void swdc_io_ffb_rumble(uint8_t period, uint8_t force) +{ + assert(swdc_io_backend != NULL); + + swdc_io_backend->ffb_rumble(period, force); +} + +void swdc_io_ffb_damper(uint8_t force) +{ + assert(swdc_io_backend != NULL); + + swdc_io_backend->ffb_damper(force); +} diff --git a/swdcio/swdcio.def b/swdcio/swdcio.def index ca63aca..b90faba 100644 --- a/swdcio/swdcio.def +++ b/swdcio/swdcio.def @@ -5,3 +5,12 @@ EXPORTS swdc_io_get_opbtns swdc_io_get_gamebtns swdc_io_get_analogs + swdc_io_led_init + swdc_io_led_set_fet_output + swdc_io_led_gs_update + swdc_io_led_set_leds + swdc_io_ffb_init + swdc_io_ffb_toggle + swdc_io_ffb_constant_force + swdc_io_ffb_rumble + swdc_io_ffb_damper diff --git a/swdcio/swdcio.h b/swdcio/swdcio.h index 7155273..6843634 100644 --- a/swdcio/swdcio.h +++ b/swdcio/swdcio.h @@ -2,6 +2,7 @@ #include +#include #include enum { @@ -26,6 +27,17 @@ enum { SWDC_IO_GAMEBTN_STEERING_PADDLE_RIGHT = 0x800, }; +enum { + /* These are the bitmasks to use when checking which + lights are triggered on incoming IO4 GPIO writes. */ + SWDC_IO_LED_START = 1 << 31, + SWDC_IO_LED_VIEW_CHANGE = 1 << 30, + SWDC_IO_LED_UP = 1 << 25, + SWDC_IO_LED_DOWN = 1 << 24, + SWDC_IO_LED_LEFT = 1 << 23, + SWDC_IO_LED_RIGHT = 1 << 22, +}; + struct swdc_io_analog_state { /* Current steering wheel position, where zero is the centered position. @@ -45,7 +57,7 @@ struct swdc_io_analog_state { uint16_t brake; }; -/* Get the version of the IDAC IO API that this DLL supports. This +/* Get the version of the SWDC IO API that this DLL supports. This function should return a positive 16-bit integer, where the high byte is the major version and the low byte is the minor version (as defined by the Semantic Versioning standard). @@ -89,3 +101,107 @@ void swdc_io_get_gamebtns(uint16_t *gamebtn); Minimum API version: 0x0100 */ void swdc_io_get_analogs(struct swdc_io_analog_state *out); + +/* Initialize LED emulation. This function will be called before any + other swdc_io_led_*() function calls. + + All subsequent calls may originate from arbitrary threads and some may + overlap with each other. Ensuring synchronization inside your IO DLL is + your responsibility. + + Minimum API version: 0x0101 */ + +HRESULT swdc_io_led_init(void); + +/* Update the FET outputs. rgb is a pointer to an array up to 3 bytes. + + The following bits are used to control the FET outputs: + [0]: LEFT SEAT LED + [1]: RIGHT SEAT LED + + The LED is truned on when the byte is 255 and turned off when the byte is 0. + + Minimum API version: 0x0101 */ + +void swdc_io_led_set_fet_output(const uint8_t *rgb); + +/* Update the RGB LEDs. rgb is a pointer to an array up to 32 * 4 = 128 bytes. + + The LEDs are laid out as follows: + [0]: LEFT UP LED + [1-2]: LEFT CENTER LED + [3]: LEFT DOWN LED + [5]: RIGHT UP LED + [6-7]: RIGHT CENTER LED + [8]: RIGHT DOWN LED + + Each rgb value is comprised for 4 bytes in the order of R, G, B, Speed. + Speed is a value from 0 to 255, where 0 is the fastest speed and 255 is the slowest. + + Minimum API version: 0x0101 */ + +void swdc_io_led_gs_update(const uint8_t *rgb); + +/* Update the cabinet button LEDs. rgb is a pointer to an array up to 6 bytes. + + The LEDs are laid out as follows: + [0]: START LED + [1]: VIEW CHANGE LED + [2]: UP LED + [3]: DOWN LED + [4]: RIGHT LED + [5]: LEFT LED + + The LED is turned on when the byte is 255 and turned off when the byte is 0. + + Minimum API version: 0x0101 */ + +void swdc_io_led_set_leds(const uint8_t *rgb); + +/* Initialize FFB emulation. This function will be called before any + other swdc_io_ffb_*() function calls. + + This will always be called even if FFB board emulation is disabled to allow + the IO DLL to initialize any necessary resources. + + Minimum API version: 0x0102 */ + +HRESULT swdc_io_ffb_init(void); + +/* Toggle FFB emulation. If active is true, FFB emulation should be enabled. + If active is false, FFB emulation should be disabled and all FFB effects + should be stopped and released. + + Minimum API version: 0x0102 */ + +void swdc_io_ffb_toggle(bool active); + +/* Set a constant force FFB effect. + + Direction is 0 for right and 1 for left. + Force is the magnitude of the force, where 0 is no force and 127 is the + maximum force in a given direction. + + Minimum API version: 0x0102 */ + +void swdc_io_ffb_constant_force(uint8_t direction, uint8_t force); + +/* Set a (sine) periodic force FFB effect. + + Period is the period of the effect in milliseconds (not sure). + Force is the magnitude of the force, where 0 is no force and 127 is the + maximum force. + + Minimum API version: 0x0102 */ + +void swdc_io_ffb_rumble(uint8_t period, uint8_t force); + +/* Set a damper FFB effect. + + Force is the magnitude of the force, where 0 is no force and 40 is the + maximum force. Theoretically the maximum force is 127, but the game only + uses a maximum of 40. + + Minimum API version: 0x0102 */ + +void swdc_io_ffb_damper(uint8_t force); diff --git a/swdcio/xi.c b/swdcio/xi.c index eb59ce6..3c4ab9c 100644 --- a/swdcio/xi.c +++ b/swdcio/xi.c @@ -16,11 +16,22 @@ static void swdc_xi_get_gamebtns(uint16_t *gamebtn_out); static void swdc_xi_get_analogs(struct swdc_io_analog_state *out); +static HRESULT swdc_xi_ffb_init(void); +static void swdc_xi_ffb_toggle(bool active); +static void swdc_xi_ffb_constant_force(uint8_t direction, uint8_t force); +static void swdc_xi_ffb_rumble(uint8_t force, uint8_t period); +static void swdc_xi_ffb_damper(uint8_t force); + static HRESULT swdc_xi_config_apply(const struct swdc_xi_config *cfg); static const struct swdc_io_backend swdc_xi_backend = { - .get_gamebtns = swdc_xi_get_gamebtns, - .get_analogs = swdc_xi_get_analogs, + .get_gamebtns = swdc_xi_get_gamebtns, + .get_analogs = swdc_xi_get_analogs, + .ffb_init = swdc_xi_ffb_init, + .ffb_toggle = swdc_xi_ffb_toggle, + .ffb_constant_force = swdc_xi_ffb_constant_force, + .ffb_rumble = swdc_xi_ffb_rumble, + .ffb_damper = swdc_xi_ffb_damper }; static bool swdc_xi_single_stick_steering; @@ -210,3 +221,35 @@ static void swdc_xi_get_analogs(struct swdc_io_analog_state *out) out->accel = xi.Gamepad.bRightTrigger << 8; out->brake = xi.Gamepad.bLeftTrigger << 8; } + +static HRESULT swdc_xi_ffb_init(void) { + return S_OK; +} + +static void swdc_xi_ffb_toggle(bool active) { + XINPUT_VIBRATION vibration; + + memset(&vibration, 0, sizeof(vibration)); + + XInputSetState(0, &vibration); +} + +static void swdc_xi_ffb_constant_force(uint8_t direction, uint8_t force) { + return; +} + +static void swdc_xi_ffb_rumble(uint8_t force, uint8_t period) { + XINPUT_VIBRATION vibration; + /* XInput max strength is 65.535, so multiply the 127.0 by 516. */ + uint16_t strength = force * 516; + + memset(&vibration, 0, sizeof(vibration)); + vibration.wLeftMotorSpeed = strength; + vibration.wRightMotorSpeed = strength; + + XInputSetState(0, &vibration); +} + +static void swdc_xi_ffb_damper(uint8_t force) { + return; +}