From c06bb408e74e9364fcc83bf5e7a7d3151b1796cd Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 30 Sep 2024 18:50:46 +0200 Subject: [PATCH] idac: add ffb emulation --- board/config.c | 8 + board/config.h | 2 + board/ffb.c | 234 +++++++++++++++++++++ board/ffb.h | 20 ++ board/meson.build | 2 + dist/idac/segatools.ini | 26 ++- hooklib/printer.c | 2 +- idachook/config.c | 1 + idachook/config.h | 1 + idachook/dllmain.c | 7 + idachook/ffb.c | 59 ++++++ idachook/ffb.h | 7 + idachook/idac-dll.c | 17 +- idachook/idac-dll.h | 5 + idachook/idachook.def | 5 + idachook/meson.build | 2 + idacio/backend.h | 5 + idacio/config.c | 26 ++- idacio/config.h | 6 +- idacio/di-dev.c | 446 +++++++++++++++++++++++++++++----------- idacio/di-dev.h | 13 +- idacio/di.c | 37 ++-- idacio/dllmain.c | 39 +++- idacio/idacio.def | 5 + idacio/idacio.h | 49 +++++ idacio/xi.c | 55 ++++- 26 files changed, 930 insertions(+), 149 deletions(-) create mode 100644 board/ffb.c create mode 100644 board/ffb.h create mode 100644 idachook/ffb.c create mode 100644 idachook/ffb.h diff --git a/board/config.c b/board/config.c index c1cd713..95eeecf 100644 --- a/board/config.c +++ b/board/config.c @@ -93,3 +93,11 @@ void vfd_config_load(struct vfd_config *cfg, const wchar_t *filename) cfg->port = GetPrivateProfileIntW(L"vfd", L"portNo", 0, filename); cfg->utf_conversion = GetPrivateProfileIntW(L"vfd", L"utfConversion", 0, filename); } + +void ffb_config_load(struct ffb_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->enable = GetPrivateProfileIntW(L"ffb", L"enable", 1, filename); +} diff --git a/board/config.h b/board/config.h index 37b5b14..34977dc 100644 --- a/board/config.h +++ b/board/config.h @@ -6,7 +6,9 @@ #include "board/io4.h" #include "board/sg-reader.h" #include "board/vfd.h" +#include "board/ffb.h" void aime_config_load(struct aime_config *cfg, const wchar_t *filename); void io4_config_load(struct io4_config *cfg, const wchar_t *filename); void vfd_config_load(struct vfd_config *cfg, const wchar_t *filename); +void ffb_config_load(struct ffb_config *cfg, const wchar_t *filename); diff --git a/board/ffb.c b/board/ffb.c new file mode 100644 index 0000000..48e15ad --- /dev/null +++ b/board/ffb.c @@ -0,0 +1,234 @@ +/* + Force Feedback Board (FFB) + + This board is used by many SEGA games to provide force feedback to the player. + It is driven by the game software over a serial connection and is used by many + games such as SEGA World Drivers Championship, Initial D Arcade, ... + + Part number in schematics is "838-15069 MOTOR DRIVE BD RS232/422 Board". + + Some observations: + The maximal strength for any effect is 127, except Damper which maxes out at 40. + The period for rumble effects is in the range 0-40. +*/ + +#include "board/ffb.h" + +#include +#include +#include + +#include "hook/iohook.h" +#include "hooklib/uart.h" +#include "util/dprintf.h" +#include "util/dump.h" + + +// request format: +// 0x?? - sync + command +// 0x?? - direction/additional command +// 0x?? - strength +// 0x?? - checksum (sum of everything except the sync byte) + +enum { + FFB_CMD_TOGGLE = 0x80, + FFB_CMD_CONSTANT_FORCE = 0x84, + FFB_CMD_RUMBLE = 0x85, + FFB_CMD_DAMPER = 0x86, +}; + +struct ffb_hdr { + uint8_t cmd; +}; + +union ffb_req_any { + struct ffb_hdr hdr; + uint8_t bytes[3]; +}; + +static HRESULT ffb_handle_irp(struct irp *irp); + +static HRESULT ffb_req_dispatch(const union ffb_req_any *req); +static HRESULT ffb_req_toggle(const uint8_t *bytes); +static HRESULT ffb_req_constant_force(const uint8_t *bytes); +static HRESULT ffb_req_rumble(const uint8_t *bytes); +static HRESULT ffb_req_damper(const uint8_t *bytes); + +static const struct ffb_ops *ffb_ops; +static struct uart ffb_uart; + +static bool ffb_started; +static HRESULT ffb_start_hr; +static uint8_t ffb_written[4]; +static uint8_t ffb_readable[4]; + +/* Static variables to store maximum strength values */ +static uint8_t max_constant_force = 0; +static uint8_t max_rumble = 0; +static uint8_t max_period = 0; +static uint8_t max_damper = 0; + +HRESULT ffb_hook_init( + const struct ffb_config *cfg, + const struct ffb_ops *ops, + unsigned int port_no) +{ + assert(cfg != NULL); + assert(ops != NULL); + + if (!cfg->enable) { + return S_FALSE; + } + + ffb_ops = ops; + + uart_init(&ffb_uart, port_no); + ffb_uart.written.bytes = ffb_written; + ffb_uart.written.nbytes = sizeof(ffb_written); + ffb_uart.readable.bytes = ffb_readable; + ffb_uart.readable.nbytes = sizeof(ffb_readable); + + dprintf("FFB: hook enabled.\n"); + + return iohook_push_handler(ffb_handle_irp); +} + +static HRESULT ffb_handle_irp(struct irp *irp) +{ + HRESULT hr; + + assert(irp != NULL); + + if (!uart_match_irp(&ffb_uart, irp)) { + return iohook_invoke_next(irp); + } + + hr = uart_handle_irp(&ffb_uart, irp); + + if (FAILED(hr) || irp->op != IRP_OP_WRITE) { + return hr; + } + + assert(&ffb_uart.written != NULL); + assert(ffb_uart.written.bytes != NULL || ffb_uart.written.nbytes == 0); + assert(ffb_uart.written.pos <= ffb_uart.written.nbytes); + + // dprintf("FFB TX:\n"); + + hr = ffb_req_dispatch((const union ffb_req_any *) ffb_uart.written.bytes); + + if (FAILED(hr)) { + dprintf("FFB: Processing error: %x\n", (int)hr); + } + + // dump_iobuf(&ffb_uart.written); + ffb_uart.written.pos = 0; + + return hr; +} + +static HRESULT ffb_req_dispatch(const union ffb_req_any *req) +{ + switch (req->hdr.cmd) { + case FFB_CMD_TOGGLE: + return ffb_req_toggle(req->bytes); + case FFB_CMD_CONSTANT_FORCE: + return ffb_req_constant_force(req->bytes); + case FFB_CMD_RUMBLE: + return ffb_req_rumble(req->bytes); + case FFB_CMD_DAMPER: + return ffb_req_damper(req->bytes); + /* There are some test mode specfic commands which doesn't seem to be used in + game at all. The same is true for the initialization phase. */ + + default: + dprintf("FFB: Unhandled command %02x\n", req->hdr.cmd); + + return S_OK; + } +} + +static HRESULT ffb_req_toggle(const uint8_t *bytes) +{ + uint8_t activate = bytes[2]; + + if (activate == 0x01) { + dprintf("FFB: Activated\n"); + } else { + dprintf("FFB: Deactivated\n"); + } + + if (ffb_ops->toggle != NULL) { + ffb_ops->toggle(activate == 0x01); + } + + return S_OK; +} + +static HRESULT ffb_req_constant_force(const uint8_t *bytes) +{ + // dprintf("FFB: Constant force\n"); + + uint8_t direction = bytes[1]; + uint8_t force = bytes[2]; + + if (direction == 0x0) { + // Right + force = 128 - force; + } + + // Update max strength if the current force is greater + if (force > max_constant_force) { + max_constant_force = force; + } + + // dprintf("FFB: Constant Force Strength: %d (Max: %d)\n", force, max_constant_force); + if (ffb_ops->constant_force != NULL) { + ffb_ops->constant_force(direction, force); + } + + return S_OK; +} + +static HRESULT ffb_req_rumble(const uint8_t *bytes) +{ + // dprintf("FFB: Rumble\n"); + + uint8_t force = bytes[1]; + uint8_t period = bytes[2]; + + // Update max strength if the current force is greater + if (force > max_rumble) { + max_rumble = force; + } + + if (period > max_period) { + max_period = period; + } + + // dprintf("FFB: Rumble Period: %d (Min %d, Max %d), Strength: %d (Max: %d)\n", period, min_period, max_period, force, max_rumble); + if (ffb_ops->rumble != NULL) { + ffb_ops->rumble(force, period); + } + + return S_OK; +} + +static HRESULT ffb_req_damper(const uint8_t *bytes) +{ + // dprintf("FFB: Damper\n"); + + uint8_t force = bytes[2]; + + // Update max strength if the current force is greater + if (force > max_damper) { + max_damper = force; + } + + // dprintf("FFB: Damper Strength: %d (Max: %d)\n", force, max_damper); + if (ffb_ops->damper != NULL) { + ffb_ops->damper(force); + } + + return S_OK; +} diff --git a/board/ffb.h b/board/ffb.h new file mode 100644 index 0000000..af46fe3 --- /dev/null +++ b/board/ffb.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +struct ffb_config { + bool enable; +}; + +struct ffb_ops { + void (*toggle)(bool active); + void (*constant_force)(uint8_t direction, uint8_t force); + void (*rumble)(uint8_t force, uint8_t period); + void (*damper)(uint8_t force); +}; + +HRESULT ffb_hook_init( + const struct ffb_config *cfg, + const struct ffb_ops *ops, + unsigned int port_no); diff --git a/board/meson.build b/board/meson.build index b26851f..2a6cf70 100644 --- a/board/meson.build +++ b/board/meson.build @@ -50,5 +50,7 @@ board_lib = static_library( 'vfd-cmd.h', 'vfd-frame.c', 'vfd-frame.h', + 'ffb.c', + 'ffb.h' ], ) diff --git a/dist/idac/segatools.ini b/dist/idac/segatools.ini index d4c828c..da94d64 100644 --- a/dist/idac/segatools.ini +++ b/dist/idac/segatools.ini @@ -75,6 +75,11 @@ dipsw3=0 dipsw4=0 dipsw5=0 +[ffb] +; Enable force feedback (838-15069) board emulation. This is required for +; both DirectInput and XInput steering wheel effects. +enable=1 + ; ----------------------------------------------------------------------------- ; LED settings ; ----------------------------------------------------------------------------- @@ -231,6 +236,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/hooklib/printer.c b/hooklib/printer.c index 010e75a..31362dd 100644 --- a/hooklib/printer.c +++ b/hooklib/printer.c @@ -82,7 +82,7 @@ int WINAPI fwdlusb_getErrorLog(uint16_t index, uint8_t *rData, uint16_t *rResult int WINAPI chcusb_MakeThread(uint16_t maxCount); int WINAPI chcusb_open(uint16_t *rResult); -__stdcall void chcusb_close(); +void WINAPI chcusb_close(); int WINAPI chcusb_ReleaseThread(uint16_t *rResult); int WINAPI chcusb_listupPrinter(uint8_t *rIdArray); int WINAPI chcusb_listupPrinterSN(uint64_t *rSerialArray); diff --git a/idachook/config.c b/idachook/config.c index a4fe392..43455ae 100644 --- a/idachook/config.c +++ b/idachook/config.c @@ -89,6 +89,7 @@ void idac_hook_config_load( zinput_config_load(&cfg->zinput, filename); dvd_config_load(&cfg->dvd, filename); io4_config_load(&cfg->io4, filename); + ffb_config_load(&cfg->ffb, filename); led15070_config_load(&cfg->led15070, filename); indrun_config_load(&cfg->indrun, filename); } diff --git a/idachook/config.h b/idachook/config.h index b7ca6d3..baae2e5 100644 --- a/idachook/config.h +++ b/idachook/config.h @@ -19,6 +19,7 @@ struct idac_hook_config { struct aime_config aime; struct dvd_config dvd; struct io4_config io4; + struct ffb_config ffb; struct idac_dll_config dll; struct zinput_config zinput; struct led15070_config led15070; diff --git a/idachook/dllmain.c b/idachook/dllmain.c index 63b0954..3336690 100644 --- a/idachook/dllmain.c +++ b/idachook/dllmain.c @@ -28,6 +28,7 @@ #include "idachook/config.h" #include "idachook/idac-dll.h" #include "idachook/io4.h" +#include "idachook/ffb.h" #include "idachook/zinput.h" #include "platform/platform.h" @@ -84,6 +85,12 @@ static DWORD CALLBACK idac_pre_startup(void) goto fail; } + hr = idac_ffb_hook_init(&idac_hook_cfg.ffb, 1); + + if (FAILED(hr)) { + goto fail; + } + hr = led15070_hook_init(&idac_hook_cfg.led15070, idac_dll.led_init, idac_dll.led_set_fet_output, NULL, idac_dll.led_gs_update, 2, 1); diff --git a/idachook/ffb.c b/idachook/ffb.c new file mode 100644 index 0000000..8befaa6 --- /dev/null +++ b/idachook/ffb.c @@ -0,0 +1,59 @@ +#include + +#include +#include +#include +#include + +#include "board/ffb.h" + +#include "idachook/idac-dll.h" + +#include "util/dprintf.h" + +static void idac_ffb_toggle(bool active); +static void idac_ffb_constant_force(uint8_t direction, uint8_t force); +static void idac_ffb_rumble(uint8_t force, uint8_t period); +static void idac_ffb_damper(uint8_t force); + +static const struct ffb_ops idac_ffb_ops = { + .toggle = idac_ffb_toggle, + .constant_force = idac_ffb_constant_force, + .rumble = idac_ffb_rumble, + .damper = idac_ffb_damper +}; + +HRESULT idac_ffb_hook_init(const struct ffb_config *cfg, unsigned int port_no) +{ + HRESULT hr; + + assert(idac_dll.init != NULL); + + hr = ffb_hook_init(cfg, &idac_ffb_ops, port_no); + + if (FAILED(hr)) { + return hr; + } + + return idac_dll.ffb_init(); +} + +static void idac_ffb_toggle(bool active) +{ + idac_dll.ffb_toggle(active); +} + +static void idac_ffb_constant_force(uint8_t direction, uint8_t force) +{ + idac_dll.ffb_constant_force(direction, force); +} + +static void idac_ffb_rumble(uint8_t force, uint8_t period) +{ + idac_dll.ffb_rumble(force, period); +} + +static void idac_ffb_damper(uint8_t force) +{ + idac_dll.ffb_damper(force); +} diff --git a/idachook/ffb.h b/idachook/ffb.h new file mode 100644 index 0000000..aff0d2d --- /dev/null +++ b/idachook/ffb.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "board/ffb.h" + +HRESULT idac_ffb_hook_init(const struct ffb_config *cfg, unsigned int port_no); diff --git a/idachook/idac-dll.c b/idachook/idac-dll.c index 3019608..5b09d91 100644 --- a/idachook/idac-dll.c +++ b/idachook/idac-dll.c @@ -36,7 +36,22 @@ const struct dll_bind_sym idac_dll_syms[] = { }, { .sym = "idac_io_led_set_leds", .off = offsetof(struct idac_dll, led_set_leds), - } + }, { + .sym = "idac_io_ffb_init", + .off = offsetof(struct idac_dll, ffb_init), + }, { + .sym = "idac_io_ffb_toggle", + .off = offsetof(struct idac_dll, ffb_toggle), + }, { + .sym = "idac_io_ffb_constant_force", + .off = offsetof(struct idac_dll, ffb_constant_force), + }, { + .sym = "idac_io_ffb_rumble", + .off = offsetof(struct idac_dll, ffb_rumble), + }, { + .sym = "idac_io_ffb_damper", + .off = offsetof(struct idac_dll, ffb_damper), + }, }; struct idac_dll idac_dll; diff --git a/idachook/idac-dll.h b/idachook/idac-dll.h index acdce35..88ac299 100644 --- a/idachook/idac-dll.h +++ b/idachook/idac-dll.h @@ -15,6 +15,11 @@ struct idac_dll { 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 idac_dll_config { diff --git a/idachook/idachook.def b/idachook/idachook.def index 32cdffc..4653c37 100644 --- a/idachook/idachook.def +++ b/idachook/idachook.def @@ -21,3 +21,8 @@ EXPORTS idac_io_led_set_fet_output idac_io_led_gs_update idac_io_led_set_leds + idac_io_ffb_init + idac_io_ffb_toggle + idac_io_ffb_constant_force + idac_io_ffb_rumble + idac_io_ffb_damper diff --git a/idachook/meson.build b/idachook/meson.build index 8de655e..66e8602 100644 --- a/idachook/meson.build +++ b/idachook/meson.build @@ -30,5 +30,7 @@ shared_library( 'zinput.h', 'indrun.c', 'indrun.h', + 'ffb.c', + 'ffb.h', ], ) diff --git a/idacio/backend.h b/idacio/backend.h index b9833a1..ee65fae 100644 --- a/idacio/backend.h +++ b/idacio/backend.h @@ -9,4 +9,9 @@ struct idac_io_backend { void (*get_gamebtns)(uint8_t *gamebtn); void (*get_shifter)(uint8_t *gear); void (*get_analogs)(struct idac_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/idacio/config.c b/idacio/config.c index 6bf6996..f862840 100644 --- a/idacio/config.c +++ b/idacio/config.c @@ -80,13 +80,29 @@ void idac_di_config_load(struct idac_di_config *cfg, const wchar_t *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 idac_xi_config_load(struct idac_xi_config *cfg, const wchar_t *filename) diff --git a/idacio/config.h b/idacio/config.h index bbf568d..88a1d6a 100644 --- a/idacio/config.h +++ b/idacio/config.h @@ -25,7 +25,11 @@ struct idac_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 idac_xi_config { diff --git a/idacio/di-dev.c b/idacio/di-dev.c index 62cab2f..a8b6e0b 100644 --- a/idacio/di-dev.c +++ b/idacio/di-dev.c @@ -1,134 +1,39 @@ #include #include - +#include #include #include "idacio/di-dev.h" #include "util/dprintf.h" -HRESULT idac_di_dev_start(IDirectInputDevice8W *dev, HWND wnd) +const struct idac_di_config *idac_di_cfg; +static HWND idac_di_wnd; +static IDirectInputDevice8W *idac_di_dev; + +/* Individual DI Effects */ +static IDirectInputEffect *idac_di_fx; +static IDirectInputEffect *idac_di_fx_rumble; +static IDirectInputEffect *idac_di_fx_damper; + +/* Max FFB Board value is 127 */ +static const double idac_di_ffb_scale = 127.0; + +HRESULT idac_di_dev_init( + const struct idac_di_config *cfg, + IDirectInputDevice8W *dev, + HWND wnd) { HRESULT hr; assert(dev != NULL); assert(wnd != NULL); - hr = IDirectInputDevice8_SetCooperativeLevel( - dev, - wnd, - DISCL_BACKGROUND | DISCL_EXCLUSIVE); + idac_di_cfg = cfg; + idac_di_dev = dev; + idac_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 idac_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 idac_di_dev_poll( @@ -167,3 +72,312 @@ HRESULT idac_di_dev_poll( return hr; } + +HRESULT idac_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 idac_di_ffb_init(void) +{ + HRESULT hr; + + hr = idac_di_dev_start(idac_di_dev, idac_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + return S_OK; +} + +void idac_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 (idac_di_fx != NULL) { + IDirectInputEffect_Stop(idac_di_fx); + IDirectInputEffect_Release(idac_di_fx); + idac_di_fx = NULL; + } + + if (idac_di_fx_rumble != NULL) { + IDirectInputEffect_Stop(idac_di_fx_rumble); + IDirectInputEffect_Release(idac_di_fx_rumble); + idac_di_fx_rumble = NULL; + } + + if (idac_di_fx_damper != NULL) { + IDirectInputEffect_Stop(idac_di_fx_damper); + IDirectInputEffect_Release(idac_di_fx_damper); + idac_di_fx_damper = NULL; + } +} + +void idac_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 = idac_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 / idac_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 (idac_di_fx != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(idac_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(idac_di_fx); + IDirectInputEffect_Release(idac_di_fx); + idac_di_fx = NULL; + } + } + + // Create a new constant force effect + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + idac_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; + } + + idac_di_fx = obj; +} + +void idac_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 = idac_di_cfg->ffb_rumble_strength * 100; + if (ffb_strength == 0) { + return; + } + + uint32_t ffb_duration = idac_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 / idac_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 (idac_di_fx_rumble != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(idac_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(idac_di_fx_rumble); + IDirectInputEffect_Release(idac_di_fx_rumble); + idac_di_fx_rumble = NULL; + } + } + + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + idac_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; + } + + idac_di_fx_rumble = obj; +} + +void idac_di_ffb_damper(uint8_t force) +{ + /* DI expects a coefficient in the range of -10.000 to 10.000 */ + uint16_t ffb_strength = idac_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 / idac_di_ffb_scale) * ffb_strength); + cond.lNegativeCoefficient = (LONG)(((double)force / idac_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 (idac_di_fx_damper != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(idac_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(idac_di_fx_damper); + IDirectInputEffect_Release(idac_di_fx_damper); + idac_di_fx_damper = NULL; + } + } + + // Create a new damper force effect + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + idac_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; + } + + idac_di_fx_damper = obj; +} diff --git a/idacio/di-dev.h b/idacio/di-dev.h index b1559e3..f26b802 100644 --- a/idacio/di-dev.h +++ b/idacio/di-dev.h @@ -5,15 +5,26 @@ #include +#include "idacio/config.h" + union idac_di_state { DIJOYSTATE st; uint8_t bytes[sizeof(DIJOYSTATE)]; }; +HRESULT idac_di_dev_init( + const struct idac_di_config *cfg, + IDirectInputDevice8W *dev, + HWND wnd); + HRESULT idac_di_dev_start(IDirectInputDevice8W *dev, HWND wnd); -void idac_di_dev_start_fx(IDirectInputDevice8W *dev, IDirectInputEffect **out, uint16_t strength); HRESULT idac_di_dev_poll( IDirectInputDevice8W *dev, HWND wnd, union idac_di_state *out); +HRESULT idac_di_ffb_init(void); +void idac_di_ffb_toggle(bool active); +void idac_di_ffb_constant_force(uint8_t direction, uint8_t force); +void idac_di_ffb_rumble(uint8_t force, uint8_t period); +void idac_di_ffb_damper(uint8_t force); diff --git a/idacio/di.c b/idacio/di.c index 55018e8..fb50ed1 100644 --- a/idacio/di.c +++ b/idacio/di.c @@ -52,9 +52,14 @@ static const struct idac_di_axis idac_di_axes[] = { }; static const struct idac_io_backend idac_di_backend = { - .get_gamebtns = idac_di_get_buttons, - .get_shifter = idac_di_get_shifter, - .get_analogs = idac_di_get_analogs, + .get_gamebtns = idac_di_get_buttons, + .get_shifter = idac_di_get_shifter, + .get_analogs = idac_di_get_analogs, + .ffb_init = idac_di_ffb_init, + .ffb_toggle = idac_di_ffb_toggle, + .ffb_constant_force = idac_di_ffb_constant_force, + .ffb_rumble = idac_di_ffb_rumble, + .ffb_damper = idac_di_ffb_damper }; static HWND idac_di_wnd; @@ -62,7 +67,6 @@ static IDirectInput8W *idac_di_api; static IDirectInputDevice8W *idac_di_dev; static IDirectInputDevice8W *idac_di_pedals; static IDirectInputDevice8W *idac_di_shifter; -static IDirectInputEffect *idac_di_fx; static size_t idac_di_off_brake; static size_t idac_di_off_accel; static uint8_t idac_di_shift_dn; @@ -105,7 +109,7 @@ HRESULT idac_di_init( return hr; } - /* Initial D Zero has some built-in DirectInput support that is not + /* Initial D THE ARCADE has some built-in DirectInput support that is not particularly useful. idachook shorts this out by redirecting dinput8.dll to a no-op implementation of DirectInput. However, idacio does need to talk to the real operating system implementation of DirectInput without @@ -168,16 +172,12 @@ HRESULT idac_di_init( return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } - hr = idac_di_dev_start(idac_di_dev, idac_di_wnd); + hr = idac_di_dev_init(cfg, idac_di_dev, idac_di_wnd); if (FAILED(hr)) { return hr; } - // Convert the strength from 0-100 to 0-10000 for DirectInput - idac_di_dev_start_fx(idac_di_dev, &idac_di_fx, - idac_di_center_spring_strength * 100); - if (cfg->pedals_name[0] != L'\0') { hr = IDirectInput8_EnumDevices( idac_di_api, @@ -367,15 +367,24 @@ static HRESULT idac_di_config_apply(const struct idac_di_config *cfg) idac_di_gear[i] = cfg->gear[i]; } - // 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; } - idac_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/idacio/dllmain.c b/idacio/dllmain.c index fbdc7d8..f295f90 100644 --- a/idacio/dllmain.c +++ b/idacio/dllmain.c @@ -19,7 +19,7 @@ static bool idac_io_coin; uint16_t idac_io_get_api_version(void) { - return 0x0101; + return 0x0102; } HRESULT idac_io_init(void) @@ -62,6 +62,8 @@ void idac_io_get_opbtns(uint8_t *opbtn_out) opbtn = 0; + /* Common operator buttons, not backend-specific */ + if (GetAsyncKeyState(idac_io_cfg.vk_test) & 0x8000) { opbtn |= IDAC_IO_OPBTN_TEST; } @@ -159,3 +161,38 @@ void idac_io_led_set_leds(const uint8_t *rgb) return; } + +HRESULT idac_io_ffb_init(void) +{ + assert(idac_io_backend != NULL); + + return idac_io_backend->ffb_init(); +} + +void idac_io_ffb_toggle(bool active) +{ + assert(idac_io_backend != NULL); + + idac_io_backend->ffb_toggle(active); +} + +void idac_io_ffb_constant_force(uint8_t direction, uint8_t force) +{ + assert(idac_io_backend != NULL); + + idac_io_backend->ffb_constant_force(direction, force); +} + +void idac_io_ffb_rumble(uint8_t period, uint8_t force) +{ + assert(idac_io_backend != NULL); + + idac_io_backend->ffb_rumble(period, force); +} + +void idac_io_ffb_damper(uint8_t force) +{ + assert(idac_io_backend != NULL); + + idac_io_backend->ffb_damper(force); +} diff --git a/idacio/idacio.def b/idacio/idacio.def index 6b447ac..f60bacf 100644 --- a/idacio/idacio.def +++ b/idacio/idacio.def @@ -10,3 +10,8 @@ EXPORTS idac_io_led_set_fet_output idac_io_led_gs_update idac_io_led_set_leds + idac_io_ffb_init + idac_io_ffb_toggle + idac_io_ffb_constant_force + idac_io_ffb_rumble + idac_io_ffb_damper diff --git a/idacio/idacio.h b/idacio/idacio.h index 5e36b5b..513c9d1 100644 --- a/idacio/idacio.h +++ b/idacio/idacio.h @@ -2,6 +2,7 @@ #include +#include #include enum { @@ -160,3 +161,51 @@ void idac_io_led_gs_update(const uint8_t *rgb); Minimum API version: 0x0101 */ void idac_io_led_set_leds(const uint8_t *rgb); + +/* Initialize FFB emulation. This function will be called before any + other idac_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 idac_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 idac_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 idac_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 idac_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 idac_io_ffb_damper(uint8_t force); diff --git a/idacio/xi.c b/idacio/xi.c index c924153..e695561 100644 --- a/idacio/xi.c +++ b/idacio/xi.c @@ -1,5 +1,3 @@ -#include "idacio/xi.h" - #include #include #include @@ -7,22 +5,35 @@ #include #include +#include "idacio/xi.h" #include "idacio/backend.h" #include "idacio/config.h" #include "idacio/idacio.h" #include "idacio/shifter.h" + #include "util/dprintf.h" static void idac_xi_get_gamebtns(uint8_t *gamebtn_out); static void idac_xi_get_shifter(uint8_t *gear); static void idac_xi_get_analogs(struct idac_io_analog_state *out); +static HRESULT idac_xi_ffb_init(void); +static void idac_xi_ffb_toggle(bool active); +static void idac_xi_ffb_constant_force(uint8_t direction, uint8_t force); +static void idac_xi_ffb_rumble(uint8_t force, uint8_t period); +static void idac_xi_ffb_damper(uint8_t force); + static HRESULT idac_xi_config_apply(const struct idac_xi_config *cfg); static const struct idac_io_backend idac_xi_backend = { - .get_gamebtns = idac_xi_get_gamebtns, - .get_shifter = idac_xi_get_shifter, - .get_analogs = idac_xi_get_analogs, + .get_gamebtns = idac_xi_get_gamebtns, + .get_shifter = idac_xi_get_shifter, + .get_analogs = idac_xi_get_analogs, + .ffb_init = idac_xi_ffb_init, + .ffb_toggle = idac_xi_ffb_toggle, + .ffb_constant_force = idac_xi_ffb_constant_force, + .ffb_rumble = idac_xi_ffb_rumble, + .ffb_damper = idac_xi_ffb_damper }; static bool idac_xi_single_stick_steering; @@ -46,7 +57,7 @@ HRESULT idac_xi_init(const struct idac_xi_config *cfg, const struct idac_io_back return hr; } - dprintf("XInput: Using XInput controller\n"); + dprintf("IDACIO: Using XInput controller\n"); *backend = &idac_xi_backend; return S_OK; @@ -205,3 +216,35 @@ static void idac_xi_get_analogs(struct idac_io_analog_state *out) { out->accel = xi.Gamepad.bRightTrigger << 8; out->brake = xi.Gamepad.bLeftTrigger << 8; } + +static HRESULT idac_xi_ffb_init(void) { + return S_OK; +} + +static void idac_xi_ffb_toggle(bool active) { + XINPUT_VIBRATION vibration; + + memset(&vibration, 0, sizeof(vibration)); + + XInputSetState(0, &vibration); +} + +static void idac_xi_ffb_constant_force(uint8_t direction, uint8_t force) { + return; +} + +static void idac_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 idac_xi_ffb_damper(uint8_t force) { + return; +}