From c06bb408e74e9364fcc83bf5e7a7d3151b1796cd Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 30 Sep 2024 18:50:46 +0200 Subject: [PATCH 1/4] 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; +} From 2251585ef026da34e054b0d17238d266b7612ff2 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 30 Sep 2024 20:23:28 +0200 Subject: [PATCH 2/4] 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; +} From 259b763a13e4efec5ecf5f1ef67e08c4ea8aff86 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 30 Sep 2024 23:10:16 +0200 Subject: [PATCH 3/4] idz: add ffb and led emulation --- board/ffb.c | 3 +- dist/idz/segatools.ini | 35 +++- dist/idz/start.bat | 5 +- idachook/dllmain.c | 4 +- idachook/io4.c | 2 + idzhook/config.c | 38 ++++ idzhook/config.h | 5 +- idzhook/dllmain.c | 20 ++ idzhook/ffb.c | 59 ++++++ idzhook/ffb.h | 7 + idzhook/idz-dll.c | 27 +++ idzhook/idz-dll.h | 9 + idzhook/idzhook.def | 9 + idzhook/jvs.c | 39 +++- idzhook/jvs.h | 2 + idzhook/meson.build | 2 + idzio/backend.h | 5 + idzio/config.c | 27 ++- idzio/config.h | 6 +- idzio/di-dev.c | 446 ++++++++++++++++++++++++++++++----------- idzio/di-dev.h | 13 +- idzio/di.c | 29 ++- idzio/dllmain.c | 78 ++++++- idzio/idzio.def | 9 + idzio/idzio.h | 116 +++++++++++ idzio/xi.c | 43 ++++ 26 files changed, 886 insertions(+), 152 deletions(-) create mode 100644 idzhook/ffb.c create mode 100644 idzhook/ffb.h diff --git a/board/ffb.c b/board/ffb.c index 48e15ad..e7a11f7 100644 --- a/board/ffb.c +++ b/board/ffb.c @@ -138,6 +138,7 @@ static HRESULT ffb_req_dispatch(const union ffb_req_any *req) 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. */ @@ -206,7 +207,7 @@ static HRESULT ffb_req_rumble(const uint8_t *bytes) 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); + // dprintf("FFB: Rumble Period: %d (Max %d), Strength: %d (Max: %d)\n", period, max_period, force, max_rumble); if (ffb_ops->rumble != NULL) { ffb_ops->rumble(force, period); } diff --git a/dist/idz/segatools.ini b/dist/idz/segatools.ini index c37bab6..126a5b7 100644 --- a/dist/idz/segatools.ini +++ b/dist/idz/segatools.ini @@ -69,6 +69,20 @@ region=4 ; exactly one machine and set this to 0 on all others. dipsw1=1 +[ffb] +; Enable force feedback (838-15069) board emulation. This is required for +; both DirectInput and XInput steering wheel effects. +enable=1 + +; ----------------------------------------------------------------------------- +; LED settings +; ----------------------------------------------------------------------------- + +[led15070] +; Enable emulation of the 837-15070-02 controlled lights, which handle the +; cabinet and seat LEDs. +enable=1 + ; ----------------------------------------------------------------------------- ; Misc. hooks settings ; ----------------------------------------------------------------------------- @@ -212,6 +226,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 \ No newline at end of file diff --git a/dist/idz/start.bat b/dist/idz/start.bat index 2d3d9c3..d64aa9b 100644 --- a/dist/idz/start.bat +++ b/dist/idz/start.bat @@ -2,10 +2,11 @@ pushd %~dp0 -inject -k idzhook.dll InitialD0_DX11_Nu.exe +start /min "AM Daemon" inject -d -k idzhook.dll amdaemon.exe -c configDHCP_Final_Common.json configDHCP_Final_JP.json configDHCP_Final_JP_ST1.json configDHCP_Final_JP_ST2.json configDHCP_Final_EX.json configDHCP_Final_EX_ST1.json configDHCP_Final_EX_ST2.json + rem Set dipsw1=0 and uncomment the ServerBox for in store battle? rem inject -k idzhook.dll ServerBoxD8_Nu_x64.exe -inject -d -k idzhook.dll amdaemon.exe -c configDHCP_Final_Common.json configDHCP_Final_JP.json configDHCP_Final_JP_ST1.json configDHCP_Final_JP_ST2.json configDHCP_Final_EX.json configDHCP_Final_EX_ST1.json configDHCP_Final_EX_ST2.json +inject -d -k idzhook.dll InitialD0_DX11_Nu.exe taskkill /im ServerBoxD8_Nu_x64.exe > nul 2>&1 diff --git a/idachook/dllmain.c b/idachook/dllmain.c index 3336690..2e82882 100644 --- a/idachook/dllmain.c +++ b/idachook/dllmain.c @@ -5,7 +5,9 @@ USB: 837-15257 "Type 4" I/O Board COM1: 838-15069 MOTOR DRIVE BD RS232/422 Board - COM2: 837-15070-02 IC BD LED Controller Board + COM2: 837-15070-02 IC BD LED Controller Board (DIPSW2 OFF) + OR + 837-15070-04 IC BD LED Controller Board (DIPSW2 ON) COM3: 837-15286 "Gen 2" Aime Reader (DIPSW2 OFF) OR 837-15396 "Gen 3" Aime Reader (DIPSW2 ON) diff --git a/idachook/io4.c b/idachook/io4.c index c13e7a5..1e4d990 100644 --- a/idachook/io4.c +++ b/idachook/io4.c @@ -133,6 +133,8 @@ static HRESULT idac_io4_poll(void *ctx, struct io4_state *state) static HRESULT idac_io4_write_gpio(uint8_t* payload, size_t len) { + assert(idac_dll.led_set_leds != NULL); + // Just fast fail if there aren't enough bytes in the payload if (len < 3) return S_OK; diff --git a/idzhook/config.c b/idzhook/config.c index db83b9f..7948c84 100644 --- a/idzhook/config.c +++ b/idzhook/config.c @@ -18,6 +18,42 @@ #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", 0x0000, 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 idz_dll_config_load( struct idz_dll_config *cfg, const wchar_t *filename) @@ -47,6 +83,8 @@ void idz_hook_config_load( dvd_config_load(&cfg->dvd, filename); gfx_config_load(&cfg->gfx, filename); idz_dll_config_load(&cfg->dll, filename); + ffb_config_load(&cfg->ffb, filename); + led15070_config_load(&cfg->led15070, filename); zinput_config_load(&cfg->zinput, filename); } diff --git a/idzhook/config.h b/idzhook/config.h index 0684949..47174ad 100644 --- a/idzhook/config.h +++ b/idzhook/config.h @@ -5,7 +5,8 @@ #include "amex/amex.h" -#include "board/sg-reader.h" +#include "board/config.h" +#include "board/led15070.h" #include "gfxhook/gfx.h" @@ -23,6 +24,8 @@ struct idz_hook_config { struct dvd_config dvd; struct gfx_config gfx; struct idz_dll_config dll; + struct ffb_config ffb; + struct led15070_config led15070; struct zinput_config zinput; }; diff --git a/idzhook/dllmain.c b/idzhook/dllmain.c index cb2fdde..e9d807a 100644 --- a/idzhook/dllmain.c +++ b/idzhook/dllmain.c @@ -33,6 +33,7 @@ #include "idzhook/config.h" #include "idzhook/idz-dll.h" #include "idzhook/jvs.h" +#include "idzhook/ffb.h" #include "idzhook/zinput.h" #include "platform/platform.h" @@ -102,6 +103,12 @@ static DWORD CALLBACK idz_pre_startup(void) goto fail; } + hr = idz_jvs_hook_init(); + + if (FAILED(hr)) { + goto fail; + } + hr = amex_hook_init(&idz_hook_cfg.amex, idz_jvs_init); if (FAILED(hr)) { @@ -114,6 +121,19 @@ static DWORD CALLBACK idz_pre_startup(void) goto fail; } + hr = idz_ffb_hook_init(&idz_hook_cfg.ffb, 1); + + if (FAILED(hr)) { + goto fail; + } + + hr = led15070_hook_init(&idz_hook_cfg.led15070, idz_dll.led_init, + idz_dll.led_set_fet_output, NULL, idz_dll.led_gs_update, 11, 1); + + if (FAILED(hr)) { + goto fail; + } + /* Initialize debug helpers */ spike_hook_init(L".\\segatools.ini"); diff --git a/idzhook/ffb.c b/idzhook/ffb.c new file mode 100644 index 0000000..7ac1086 --- /dev/null +++ b/idzhook/ffb.c @@ -0,0 +1,59 @@ +#include + +#include +#include +#include +#include + +#include "board/ffb.h" + +#include "idzhook/idz-dll.h" + +#include "util/dprintf.h" + +static void idz_ffb_toggle(bool active); +static void idz_ffb_constant_force(uint8_t direction, uint8_t force); +static void idz_ffb_rumble(uint8_t force, uint8_t period); +static void idz_ffb_damper(uint8_t force); + +static const struct ffb_ops idz_ffb_ops = { + .toggle = idz_ffb_toggle, + .constant_force = idz_ffb_constant_force, + .rumble = idz_ffb_rumble, + .damper = idz_ffb_damper +}; + +HRESULT idz_ffb_hook_init(const struct ffb_config *cfg, unsigned int port_no) +{ + HRESULT hr; + + assert(idz_dll.jvs_init != NULL); + + hr = ffb_hook_init(cfg, &idz_ffb_ops, port_no); + + if (FAILED(hr)) { + return hr; + } + + return idz_dll.ffb_init(); +} + +static void idz_ffb_toggle(bool active) +{ + idz_dll.ffb_toggle(active); +} + +static void idz_ffb_constant_force(uint8_t direction, uint8_t force) +{ + idz_dll.ffb_constant_force(direction, force); +} + +static void idz_ffb_rumble(uint8_t force, uint8_t period) +{ + idz_dll.ffb_rumble(force, period); +} + +static void idz_ffb_damper(uint8_t force) +{ + idz_dll.ffb_damper(force); +} diff --git a/idzhook/ffb.h b/idzhook/ffb.h new file mode 100644 index 0000000..eaee98c --- /dev/null +++ b/idzhook/ffb.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "board/ffb.h" + +HRESULT idz_ffb_hook_init(const struct ffb_config *cfg, unsigned int port_no); diff --git a/idzhook/idz-dll.c b/idzhook/idz-dll.c index eb1b6e1..a497319 100644 --- a/idzhook/idz-dll.c +++ b/idzhook/idz-dll.c @@ -24,6 +24,33 @@ const struct dll_bind_sym idz_dll_syms[] = { }, { .sym = "idz_io_jvs_read_coin_counter", .off = offsetof(struct idz_dll, jvs_read_coin_counter), + }, { + .sym = "idz_io_led_init", + .off = offsetof(struct idz_dll, led_init), + }, { + .sym = "idz_io_led_set_fet_output", + .off = offsetof(struct idz_dll, led_set_fet_output), + }, { + .sym = "idz_io_led_gs_update", + .off = offsetof(struct idz_dll, led_gs_update), + }, { + .sym = "idz_io_led_set_leds", + .off = offsetof(struct idz_dll, led_set_leds), + }, { + .sym = "idz_io_ffb_init", + .off = offsetof(struct idz_dll, ffb_init), + }, { + .sym = "idz_io_ffb_toggle", + .off = offsetof(struct idz_dll, ffb_toggle), + }, { + .sym = "idz_io_ffb_constant_force", + .off = offsetof(struct idz_dll, ffb_constant_force), + }, { + .sym = "idz_io_ffb_rumble", + .off = offsetof(struct idz_dll, ffb_rumble), + }, { + .sym = "idz_io_ffb_damper", + .off = offsetof(struct idz_dll, ffb_damper), } }; diff --git a/idzhook/idz-dll.h b/idzhook/idz-dll.h index e67cb45..e827f8e 100644 --- a/idzhook/idz-dll.h +++ b/idzhook/idz-dll.h @@ -11,6 +11,15 @@ struct idz_dll { void (*jvs_read_buttons)(uint8_t *opbtn, uint8_t *gamebtn); void (*jvs_read_shifter)(uint8_t *gear); void (*jvs_read_coin_counter)(uint16_t *total); + 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 idz_dll_config { diff --git a/idzhook/idzhook.def b/idzhook/idzhook.def index d8db3b0..53e01c6 100644 --- a/idzhook/idzhook.def +++ b/idzhook/idzhook.def @@ -22,3 +22,12 @@ EXPORTS idz_io_jvs_read_buttons idz_io_jvs_read_coin_counter idz_io_jvs_read_shifter + idz_io_led_init + idz_io_led_set_fet_output + idz_io_led_gs_update + idz_io_led_set_leds + idz_io_ffb_init + idz_io_ffb_toggle + idz_io_ffb_constant_force + idz_io_ffb_rumble + idz_io_ffb_damper diff --git a/idzhook/jvs.c b/idzhook/jvs.c index ab2f6aa..3531d55 100644 --- a/idzhook/jvs.c +++ b/idzhook/jvs.c @@ -24,11 +24,13 @@ static void idz_jvs_read_coin_counter( void *ctx, uint8_t slot_no, uint16_t *out); +static void idz_jvs_write_gpio(void *ctx, uint32_t state); static const struct io3_ops idz_jvs_io3_ops = { .read_switches = idz_jvs_read_switches, .read_analogs = idz_jvs_read_analogs, .read_coin_counter = idz_jvs_read_coin_counter, + .write_gpio = idz_jvs_write_gpio }; static const uint16_t idz_jvs_gear_signals[] = { @@ -50,21 +52,20 @@ static const uint16_t idz_jvs_gear_signals[] = { static struct io3 idz_jvs_io3; +HRESULT idz_jvs_hook_init(void) +{ + HRESULT hr; + + assert(idz_dll.jvs_init != NULL); + + return idz_dll.jvs_init(); +} + HRESULT idz_jvs_init(struct jvs_node **out) { HRESULT hr; assert(out != NULL); - assert(idz_dll.jvs_init != NULL); - - dprintf("JVS I/O: Starting Initial D Zero backend DLL\n"); - hr = idz_dll.jvs_init(); - - if (FAILED(hr)) { - dprintf("JVS I/O: Backend error, I/O disconnected; %x\n", (int) hr); - - return hr; - } io3_init(&idz_jvs_io3, NULL, &idz_jvs_io3_ops, NULL); *out = io3_to_jvs_node(&idz_jvs_io3); @@ -175,3 +176,21 @@ static void idz_jvs_read_coin_counter( idz_dll.jvs_read_coin_counter(out); } +static void idz_jvs_write_gpio(void *ctx, uint32_t state) +{ + assert(idz_dll.led_set_leds != NULL); + + // 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] = { + state & IDZ_IO_LED_START ? 0xFF : 0x00, + state & IDZ_IO_LED_VIEW_CHANGE ? 0xFF : 0x00, + state & IDZ_IO_LED_UP ? 0xFF : 0x00, + state & IDZ_IO_LED_DOWN ? 0xFF : 0x00, + state & IDZ_IO_LED_RIGHT ? 0xFF : 0x00, + state & IDZ_IO_LED_LEFT ? 0xFF : 0x00, + }; + + idz_dll.led_set_leds(rgb_out); +} diff --git a/idzhook/jvs.h b/idzhook/jvs.h index 6a7c72a..82de143 100644 --- a/idzhook/jvs.h +++ b/idzhook/jvs.h @@ -4,4 +4,6 @@ #include "jvs/jvs-bus.h" +HRESULT idz_jvs_hook_init(void); + HRESULT idz_jvs_init(struct jvs_node **root); diff --git a/idzhook/meson.build b/idzhook/meson.build index ab90815..eb9ba6c 100644 --- a/idzhook/meson.build +++ b/idzhook/meson.build @@ -32,5 +32,7 @@ shared_library( 'jvs.h', 'zinput.c', 'zinput.h', + 'ffb.c', + 'ffb.h', ], ) diff --git a/idzio/backend.h b/idzio/backend.h index e0a958f..1993076 100644 --- a/idzio/backend.h +++ b/idzio/backend.h @@ -8,4 +8,9 @@ struct idz_io_backend { void (*jvs_read_buttons)(uint8_t *gamebtn); void (*jvs_read_shifter)(uint8_t *gear); void (*jvs_read_analogs)(struct idz_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/idzio/config.c b/idzio/config.c index aec8261..369a4c5 100644 --- a/idzio/config.c +++ b/idzio/config.c @@ -78,12 +78,29 @@ void idz_di_config_load(struct idz_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 idz_xi_config_load(struct idz_xi_config *cfg, const wchar_t *filename) diff --git a/idzio/config.h b/idzio/config.h index 155f797..7a17f00 100644 --- a/idzio/config.h +++ b/idzio/config.h @@ -23,7 +23,11 @@ struct idz_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 idz_xi_config { diff --git a/idzio/di-dev.c b/idzio/di-dev.c index 26cf6e3..053eb9c 100644 --- a/idzio/di-dev.c +++ b/idzio/di-dev.c @@ -1,134 +1,39 @@ #include #include - +#include #include #include "idzio/di-dev.h" #include "util/dprintf.h" -HRESULT idz_di_dev_start(IDirectInputDevice8W *dev, HWND wnd) +const struct idz_di_config *idz_di_cfg; +static HWND idz_di_wnd; +static IDirectInputDevice8W *idz_di_dev; + +/* Individual DI Effects */ +static IDirectInputEffect *idz_di_fx; +static IDirectInputEffect *idz_di_fx_rumble; +static IDirectInputEffect *idz_di_fx_damper; + +/* Max FFB Board value is 127 */ +static const double idz_di_ffb_scale = 127.0; + +HRESULT idz_di_dev_init( + const struct idz_di_config *cfg, + IDirectInputDevice8W *dev, + HWND wnd) { HRESULT hr; assert(dev != NULL); assert(wnd != NULL); - hr = IDirectInputDevice8_SetCooperativeLevel( - dev, - wnd, - DISCL_BACKGROUND | DISCL_EXCLUSIVE); + idz_di_cfg = cfg; + idz_di_dev = dev; + idz_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 idz_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 idz_di_dev_poll( @@ -167,3 +72,312 @@ HRESULT idz_di_dev_poll( return hr; } + +HRESULT idz_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 idz_di_ffb_init(void) +{ + HRESULT hr; + + hr = idz_di_dev_start(idz_di_dev, idz_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + return S_OK; +} + +void idz_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 (idz_di_fx != NULL) { + IDirectInputEffect_Stop(idz_di_fx); + IDirectInputEffect_Release(idz_di_fx); + idz_di_fx = NULL; + } + + if (idz_di_fx_rumble != NULL) { + IDirectInputEffect_Stop(idz_di_fx_rumble); + IDirectInputEffect_Release(idz_di_fx_rumble); + idz_di_fx_rumble = NULL; + } + + if (idz_di_fx_damper != NULL) { + IDirectInputEffect_Stop(idz_di_fx_damper); + IDirectInputEffect_Release(idz_di_fx_damper); + idz_di_fx_damper = NULL; + } +} + +void idz_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 = idz_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 / idz_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 (idz_di_fx != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(idz_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(idz_di_fx); + IDirectInputEffect_Release(idz_di_fx); + idz_di_fx = NULL; + } + } + + // Create a new constant force effect + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + idz_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; + } + + idz_di_fx = obj; +} + +void idz_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 = idz_di_cfg->ffb_rumble_strength * 100; + if (ffb_strength == 0) { + return; + } + + uint32_t ffb_duration = idz_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 / idz_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 (idz_di_fx_rumble != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(idz_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(idz_di_fx_rumble); + IDirectInputEffect_Release(idz_di_fx_rumble); + idz_di_fx_rumble = NULL; + } + } + + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + idz_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; + } + + idz_di_fx_rumble = obj; +} + +void idz_di_ffb_damper(uint8_t force) +{ + /* DI expects a coefficient in the range of -10.000 to 10.000 */ + uint16_t ffb_strength = idz_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 / idz_di_ffb_scale) * ffb_strength); + cond.lNegativeCoefficient = (LONG)(((double)force / idz_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 (idz_di_fx_damper != NULL) { + // Try to update the existing effect + hr = IDirectInputEffect_SetParameters(idz_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(idz_di_fx_damper); + IDirectInputEffect_Release(idz_di_fx_damper); + idz_di_fx_damper = NULL; + } + } + + // Create a new damper force effect + IDirectInputEffect *obj; + hr = IDirectInputDevice8_CreateEffect( + idz_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; + } + + idz_di_fx_damper = obj; +} diff --git a/idzio/di-dev.h b/idzio/di-dev.h index 9b3d03f..783ec66 100644 --- a/idzio/di-dev.h +++ b/idzio/di-dev.h @@ -5,15 +5,26 @@ #include +#include "idzio/config.h" + union idz_di_state { DIJOYSTATE st; uint8_t bytes[sizeof(DIJOYSTATE)]; }; +HRESULT idz_di_dev_init( + const struct idz_di_config *cfg, + IDirectInputDevice8W *dev, + HWND wnd); + HRESULT idz_di_dev_start(IDirectInputDevice8W *dev, HWND wnd); -void idz_di_dev_start_fx(IDirectInputDevice8W *dev, IDirectInputEffect **out, uint16_t strength); HRESULT idz_di_dev_poll( IDirectInputDevice8W *dev, HWND wnd, union idz_di_state *out); +HRESULT idz_di_ffb_init(void); +void idz_di_ffb_toggle(bool active); +void idz_di_ffb_constant_force(uint8_t direction, uint8_t force); +void idz_di_ffb_rumble(uint8_t force, uint8_t period); +void idz_di_ffb_damper(uint8_t force); diff --git a/idzio/di.c b/idzio/di.c index e0cb4c6..8d8be41 100644 --- a/idzio/di.c +++ b/idzio/di.c @@ -55,6 +55,11 @@ static const struct idz_io_backend idz_di_backend = { .jvs_read_buttons = idz_di_jvs_read_buttons, .jvs_read_shifter = idz_di_jvs_read_shifter, .jvs_read_analogs = idz_di_jvs_read_analogs, + .ffb_init = idz_di_ffb_init, + .ffb_toggle = idz_di_ffb_toggle, + .ffb_constant_force = idz_di_ffb_constant_force, + .ffb_rumble = idz_di_ffb_rumble, + .ffb_damper = idz_di_ffb_damper }; static HWND idz_di_wnd; @@ -73,7 +78,6 @@ static uint8_t idz_di_gear[6]; static bool idz_di_use_pedals; static bool idz_di_reverse_brake_axis; static bool idz_di_reverse_accel_axis; -static uint16_t idz_di_center_spring_strength; HRESULT idz_di_init( const struct idz_di_config *cfg, @@ -166,16 +170,12 @@ HRESULT idz_di_init( return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } - hr = idz_di_dev_start(idz_di_dev, idz_di_wnd); + hr = idz_di_dev_init(cfg, idz_di_dev, idz_di_wnd); if (FAILED(hr)) { return hr; } - // Convert the strength from 0-100 to 0-10000 for DirectInput - idz_di_dev_start_fx(idz_di_dev, &idz_di_fx, - idz_di_center_spring_strength * 100); - if (cfg->pedals_name[0] != L'\0') { hr = IDirectInput8_EnumDevices( idz_di_api, @@ -349,15 +349,24 @@ static HRESULT idz_di_config_apply(const struct idz_di_config *cfg) idz_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; } - idz_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/idzio/dllmain.c b/idzio/dllmain.c index ad94747..5c8592d 100644 --- a/idzio/dllmain.c +++ b/idzio/dllmain.c @@ -20,7 +20,7 @@ static uint16_t idz_io_coins; uint16_t idz_io_get_api_version(void) { - return 0x0100; + return 0x0102; } HRESULT idz_io_jvs_init(void) @@ -123,3 +123,79 @@ void idz_io_jvs_read_coin_counter(uint16_t *out) *out = idz_io_coins; } + +HRESULT idz_io_led_init(void) +{ + return S_OK; +} + +void idz_io_led_set_fet_output(const uint8_t *rgb) +{ +#if 0 + dprintf("IDZ LED: LEFT SEAT LED: %02X\n", rgb[0]); + dprintf("IDZ LED: RIGHT SEAT LED: %02X\n", rgb[1]); +#endif + + return; +} + +void idz_io_led_gs_update(const uint8_t *rgb) +{ +#if 0 + for (int i = 0; i < 9; i++) { + dprintf("IDZ 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 idz_io_led_set_leds(const uint8_t *rgb) +{ +#if 0 + dprintf("IDZ LED: START: %02X\n", rgb[0]); + dprintf("IDZ LED: VIEW CHANGE: %02X\n", rgb[1]); + dprintf("IDZ LED: UP: %02X\n", rgb[2]); + dprintf("IDZ LED: DOWN: %02X\n", rgb[3]); + dprintf("IDZ LED: RIGHT: %02X\n", rgb[4]); + dprintf("IDZ LED: LEFT: %02X\n", rgb[5]); +#endif + + return; +} + +HRESULT idz_io_ffb_init(void) +{ + assert(idz_io_backend != NULL); + + return idz_io_backend->ffb_init(); +} + +void idz_io_ffb_toggle(bool active) +{ + assert(idz_io_backend != NULL); + + idz_io_backend->ffb_toggle(active); +} + +void idz_io_ffb_constant_force(uint8_t direction, uint8_t force) +{ + assert(idz_io_backend != NULL); + + idz_io_backend->ffb_constant_force(direction, force); +} + +void idz_io_ffb_rumble(uint8_t period, uint8_t force) +{ + assert(idz_io_backend != NULL); + + idz_io_backend->ffb_rumble(period, force); +} + +void idz_io_ffb_damper(uint8_t force) +{ + assert(idz_io_backend != NULL); + + idz_io_backend->ffb_damper(force); +} diff --git a/idzio/idzio.def b/idzio/idzio.def index b177000..4d66148 100644 --- a/idzio/idzio.def +++ b/idzio/idzio.def @@ -6,3 +6,12 @@ EXPORTS idz_io_jvs_read_buttons idz_io_jvs_read_coin_counter idz_io_jvs_read_shifter + idz_io_led_init + idz_io_led_set_fet_output + idz_io_led_gs_update + idz_io_led_set_leds + idz_io_ffb_init + idz_io_ffb_toggle + idz_io_ffb_constant_force + idz_io_ffb_rumble + idz_io_ffb_damper diff --git a/idzio/idzio.h b/idzio/idzio.h index 395b012..8fd01d8 100644 --- a/idzio/idzio.h +++ b/idzio/idzio.h @@ -14,6 +14,7 @@ #include +#include #include enum { @@ -30,6 +31,17 @@ enum { IDZ_IO_GAMEBTN_VIEW_CHANGE = 0x20, }; +enum { + /* These are the bitmasks to use when checking which + lights are triggered on incoming IO4 GPIO writes. */ + IDZ_IO_LED_START = 1 << 7, + IDZ_IO_LED_VIEW_CHANGE = 1 << 6, + IDZ_IO_LED_UP = 1 << 1, + IDZ_IO_LED_DOWN = 1 << 0, + IDZ_IO_LED_RIGHT = 1 << 14, + IDZ_IO_LED_LEFT = 1 << 15 +}; + struct idz_io_analog_state { /* Current steering wheel position, where zero is the centered position. @@ -104,3 +116,107 @@ void idz_io_jvs_read_shifter(uint8_t *gear); Minimum API version: 0x0100 */ void idz_io_jvs_read_coin_counter(uint16_t *total); + +/* Initialize LED emulation. This function will be called before any + other idz_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 idz_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 idz_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 idz_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 idz_io_led_set_leds(const uint8_t *rgb); + +/* Initialize FFB emulation. This function will be called before any + other idz_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 idz_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 idz_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 idz_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 idz_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 idz_io_ffb_damper(uint8_t force); diff --git a/idzio/xi.c b/idzio/xi.c index 4a8391d..db3a94d 100644 --- a/idzio/xi.c +++ b/idzio/xi.c @@ -18,12 +18,23 @@ static void idz_xi_jvs_read_buttons(uint8_t *gamebtn_out); static void idz_xi_jvs_read_shifter(uint8_t *gear); static void idz_xi_jvs_read_analogs(struct idz_io_analog_state *out); +static HRESULT idz_xi_ffb_init(void); +static void idz_xi_ffb_toggle(bool active); +static void idz_xi_ffb_constant_force(uint8_t direction, uint8_t force); +static void idz_xi_ffb_rumble(uint8_t force, uint8_t period); +static void idz_xi_ffb_damper(uint8_t force); + static HRESULT idz_xi_config_apply(const struct idz_xi_config *cfg); static const struct idz_io_backend idz_xi_backend = { .jvs_read_buttons = idz_xi_jvs_read_buttons, .jvs_read_shifter = idz_xi_jvs_read_shifter, .jvs_read_analogs = idz_xi_jvs_read_analogs, + .ffb_init = idz_xi_ffb_init, + .ffb_toggle = idz_xi_ffb_toggle, + .ffb_constant_force = idz_xi_ffb_constant_force, + .ffb_rumble = idz_xi_ffb_rumble, + .ffb_damper = idz_xi_ffb_damper }; static bool idz_xi_single_stick_steering; @@ -210,3 +221,35 @@ static void idz_xi_jvs_read_analogs(struct idz_io_analog_state *out) out->accel = xi.Gamepad.bRightTrigger << 8; out->brake = xi.Gamepad.bLeftTrigger << 8; } + +static HRESULT idz_xi_ffb_init(void) { + return S_OK; +} + +static void idz_xi_ffb_toggle(bool active) { + XINPUT_VIBRATION vibration; + + memset(&vibration, 0, sizeof(vibration)); + + XInputSetState(0, &vibration); +} + +static void idz_xi_ffb_constant_force(uint8_t direction, uint8_t force) { + return; +} + +static void idz_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 idz_xi_ffb_damper(uint8_t force) { + return; +} From 5f817c8a36a187e6ba349c1aa7650bcd5b9df2ec Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Mon, 30 Sep 2024 23:17:37 +0200 Subject: [PATCH 4/4] swdc: minor improvements --- swdchook/config.c | 21 +++++++++++---------- swdchook/dllmain.c | 7 ++++--- swdchook/io4.c | 2 ++ 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/swdchook/config.c b/swdchook/config.c index 62115fa..269a450 100644 --- a/swdchook/config.c +++ b/swdchook/config.c @@ -30,7 +30,7 @@ void led15070_config_load(struct led15070_config *cfg, const wchar_t *filename) GetPrivateProfileStringW( L"led15070", L"boardNumber", - L"15070-02", + L"15070-04", tmpstr, _countof(tmpstr), filename); @@ -66,6 +66,14 @@ void swdc_dll_config_load( filename); } +void zinput_config_load(struct zinput_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->enable = GetPrivateProfileIntW(L"zinput", L"enable", 1, filename); +} + void swdc_hook_config_load( struct swdc_hook_config *cfg, const wchar_t *filename) @@ -79,14 +87,7 @@ void swdc_hook_config_load( zinput_config_load(&cfg->zinput, filename); dvd_config_load(&cfg->dvd, filename); io4_config_load(&cfg->io4, filename); - vfd_config_load(&cfg->vfd, filename); + ffb_config_load(&cfg->ffb, filename); led15070_config_load(&cfg->led15070, filename); -} - -void zinput_config_load(struct zinput_config *cfg, const wchar_t *filename) -{ - assert(cfg != NULL); - assert(filename != NULL); - - cfg->enable = GetPrivateProfileIntW(L"zinput", L"enable", 1, filename); + vfd_config_load(&cfg->vfd, filename); } diff --git a/swdchook/dllmain.c b/swdchook/dllmain.c index f9c6081..bd6a1b3 100644 --- a/swdchook/dllmain.c +++ b/swdchook/dllmain.c @@ -8,8 +8,8 @@ WITH 838-15416 Indicator BD LED Board COM1: 838-15069 MOTOR DRIVE BD RS232/422 board - COM2: 837-15396 "Gen 3" Aime reader - COM3: 837-15070-04 IC BD LED controller board + COM2: 837-15070-04 IC BD LED controller board + COM3: 837-15396 "Gen 3" Aime reader COM4: 200-6275 VFD GP1232A02A FUTABA board */ @@ -97,7 +97,8 @@ static DWORD CALLBACK swdc_pre_startup(void) if (FAILED(hr)) { goto fail; } - + + /* Not working, different board -04 instead of -02? */ 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); diff --git a/swdchook/io4.c b/swdchook/io4.c index 5221f63..be1d4b9 100644 --- a/swdchook/io4.c +++ b/swdchook/io4.c @@ -177,6 +177,8 @@ static HRESULT swdc_io4_poll(void *ctx, struct io4_state *state) { static HRESULT swdc_io4_write_gpio(uint8_t* payload, size_t len) { + assert(swdc_dll.led_set_leds != NULL); + // Just fast fail if there aren't enough bytes in the payload if (len < 3) return S_OK;