diff --git a/core/hw/maple/maple_jvs.cpp b/core/hw/maple/maple_jvs.cpp index c8fe28856..17b4f8610 100644 --- a/core/hw/maple/maple_jvs.cpp +++ b/core/hw/maple/maple_jvs.cpp @@ -24,6 +24,7 @@ #include "cfg/option.h" #include "network/output.h" #include "hw/naomi/printer.h" +#include "input/haptic.h" #include #include @@ -662,8 +663,10 @@ protected: }; // 837-13844 jvs board wired to force-feedback drive board -// 838-13843: f355 -// 838-13992: 18 wheeler +// 838-13843: f355 (ROM EPR-21867) +// 838-13992: 18 wheeler (ROM EPR-23000) +// with 838-12912-01 servo board (same as model3) +// https://github.com/njz3/vJoyIOFeederWithFFB/blob/master/DRIVEBOARD.md class jvs_837_13844_racing : public jvs_837_13844_motor_board { public: @@ -675,13 +678,51 @@ public: void serialize(Serializer& ser) const override { ser << testMode; + ser << damper_high; + ser << active; + ser << motorPower; + ser << springSat; + ser << springSpeed; + ser << torque; + ser << damper; jvs_837_13844_motor_board::serialize(ser); } void deserialize(Deserializer& deser) override { if (deser.version() >= Deserializer::V31) deser >> testMode; + else + testMode = false; + if (deser.version() >= Deserializer::V51) + { + deser >> damper_high; + deser >> active; + deser >> motorPower; + deser >> springSat; + deser >> springSpeed; + deser >> torque; + deser >> damper; + } + else + { + damper_high = 8; + active = false; + motorPower = 1.f; + springSat = 0.f; + springSpeed = 0.f; + torque = 0.f; + damper = 0.f; + } jvs_837_13844_motor_board::deserialize(deser); + if (active) + { + haptic::setSpring(0, springSat, springSpeed); + haptic::setTorque(0, torque); + haptic::setDamper(0, damper, damper / 2.f); + } + else { + haptic::stopAll(0); + } } protected: @@ -689,31 +730,95 @@ protected: { in = ~in; networkOutput.output("m3ffb", in); - // E0: stop motor - // E3: roll right - // EB: roll left - // Dn: set wheel high-order? - // Cn: set wheel low-order? - // 18 wheeler: ff, fe, 3f, 49, 67 - // d8, c0, e0, d0 - // 4b, 4a, 9f, 4b, 4d, 4e, 4f, 45, 45, 4f, a3, 4d, ..., 9f, 4c, u8 out = 0; switch (in) { + case 0: + case 1: + case 2: + // 18wheeler: 0 light (60%), 1 normal (80%), 2 heavy (100%) + if (!active) + motorPower = 0.6f + in * 0.2f; + break; + case 0xf0: + active = false; testMode = true; break; + case 0xfe: + active = true; + break; + case 0xff: testMode = false; + active = false; + haptic::stopAll(0); break; case 0xf1: - out = 0x10; + out = 0x10; // needed by f355 + break; + + case 0xfa: + motorPower = 1.f; // f355: 100% + break; + case 0xfb: + motorPower = 0.9f; // f355: 90% + break; + case 0xfc: + motorPower = 0.8f; // f355: 80% + break; + case 0xfd: + motorPower = 0.6f; // f355: 60% break; default: + if (active) + { + if (in >= 0x40 && in <= 0x7f) + { + // Spring + // bits 0-3 sets the strength of the spring effect + // bits 4-5 selects a table scaling the effect given the deflection: + // (from the f355 rom EPR-21867) + // 0: large deadzone then abrupt scaling (0 (* 129), 10, 20, 30, 40, 50, 60, 70, 7F) + // used by 18wheeler in game but with a different rom (TODO reveng) + // other tables scale linearly: + // 1: light speed (96 steps from 0 to 7f) + // 2: medium speed (48 steps, default) + // 3: high speed (32 steps) + springSat = (in & 0xf) / 15.f * motorPower; + const int speedSel = (in >> 4) & 3; + springSpeed = speedSel == 3 ? 1.f : speedSel == 2 ? 0.67f : speedSel == 1 ? 0.33f : 0.67f; + haptic::setSpring(0, springSat, springSpeed); + } + else if (in >= 0x80 && in <= 0xbf) + { + // Rumble + const float v = (in & 0x3f) / 63.f * motorPower / 2.f; // additional 0.5 factor to soften it + MapleConfigMap::UpdateVibration(0, v, 0.f, 50); // duration? + } + else if (in >= 0xe0 && in <= 0xef) + { + // Test menu roll left/right (not used in game) + torque = (in < 0xe8 ? (0xe0 - in) : (in - 0xe8)) / 7.f; + haptic::setTorque(0, torque); + } + else if ((in & 0xf0) == 0xc0) + { + // Damper? more likely Friction + // activated in f355 when turning the wheel while stopped + // high-order bits are set with Dn, low-order bits with Cn. Only the later commits the change. + const u8 v = (damper_high << 4) | (in & 0xf); + damper = std::abs(v - 0x80) / 128.f * motorPower; + haptic::setDamper(0, damper, damper / 2.f); + } + else if ((in & 0xf0) == 0xd0) { + damper_high = in & 0xf; + } + } break; } if (testMode) @@ -729,6 +834,13 @@ protected: private: bool testMode = false; + u8 damper_high = 8; + bool active = false; + float motorPower = 1.f; + float springSat = 0.f; + float springSpeed = 0.f; + float torque = 0.f; + float damper = 0.f; }; // 18 Wheeler: fake the drive board and limit the wheel analog value diff --git a/core/hw/naomi/naomi.cpp b/core/hw/naomi/naomi.cpp index 8eb2b0c98..7fe4e2e5b 100644 --- a/core/hw/naomi/naomi.cpp +++ b/core/hw/naomi/naomi.cpp @@ -33,6 +33,7 @@ #include "oslib/oslib.h" #include "printer.h" #include "hw/flashrom/x76f100.h" +#include "input/haptic.h" #include @@ -42,9 +43,6 @@ Multiboard *multiboard; static X76F100SerialFlash mainSerialId; static X76F100SerialFlash romSerialId; -static u8 midiTxBuf[4]; -static u32 midiTxBufIndex; - static int dmaSchedId = -1; void NaomiBoardIDWrite(const u16 data) @@ -219,6 +217,7 @@ void naomi_reg_Term() if (dmaSchedId != -1) sh4_sched_unregister(dmaSchedId); dmaSchedId = -1; + midiffb::term(); } void naomi_reg_Reset(bool hard) @@ -248,6 +247,7 @@ void naomi_reg_Reset(bool hard) } else if (multiboard != nullptr) multiboard->reset(); + midiffb::reset(); } static u8 aw_maple_devs; @@ -334,10 +334,14 @@ void libExtDevice_WriteMem_A0_006(u32 addr, u32 data, u32 size) if ((u8)data != awDigitalOuput) { if (atomiswaveForceFeedback) + { // Wheel force feedback: // bit 0 direction (0 pos, 1 neg) // bit 1-4 strength networkOutput.output("awffb", (u8)data); + // This really needs to be soften + haptic::setTorque(0, (data & 1 ? -1.f : 1.f) * ((data >> 1) & 0xf) / 15.f * 0.5f); + } else { u8 changes = data ^ awDigitalOuput; @@ -358,8 +362,6 @@ void libExtDevice_WriteMem_A0_006(u32 addr, u32 data, u32 size) INFO_LOG(NAOMI, "Unhandled write @ %x (%d): %x", addr, size, data); } -static bool ffbCalibrating; - void naomi_Serialize(Serializer& ser) { mainSerialId.serialize(ser); @@ -367,10 +369,8 @@ void naomi_Serialize(Serializer& ser) ser << aw_maple_devs; ser << coin_chute_time; ser << aw_ram_test_skipped; - ser << midiTxBuf; - ser << midiTxBufIndex; // TODO serialize m3comm? - ser << ffbCalibrating; + midiffb::serialize(ser); sh4_sched_serialize(ser, dmaSchedId); } void naomi_Deserialize(Deserializer& deser) @@ -415,23 +415,24 @@ void naomi_Deserialize(Deserializer& deser) deser >> coin_chute_time; deser >> aw_ram_test_skipped; } - if (deser.version() >= Deserializer::V27) - { - deser >> midiTxBuf; - deser >> midiTxBufIndex; - } - else - { - midiTxBufIndex = 0; - } - if (deser.version() >= Deserializer::V34) - deser >> ffbCalibrating; - else - ffbCalibrating = false; + midiffb::deserialize(deser); if (deser.version() >= Deserializer::V45) sh4_sched_deserialize(deser, dmaSchedId); } +namespace midiffb { + +static bool initialized; +static u8 midiTxBuf[4]; +static u32 midiTxBufIndex; +static bool calibrating; +static float damperParam; +static float damperSpeed; +static float power = 0.8f; +static bool active; +static float position = 8192.f; +static float torque; + static void midiSend(u8 b1, u8 b2, u8 b3) { aica::midiSend(b1); @@ -440,24 +441,108 @@ static void midiSend(u8 b1, u8 b2, u8 b3) aica::midiSend((b1 ^ b2 ^ b3) & 0x7f); } -static void forceFeedbackMidiReceiver(u8 data) +// https://www.arcade-projects.com/threads/force-feedback-translator-sega-midi-sega-rs422-and-namco-rs232.924/ +static void midiReceiver(u8 data) { - static float position = 8192.f; - static float torque; + // fake position used during calibration position = std::min(16383.f, std::max(0.f, position + torque)); if (data & 0x80) midiTxBufIndex = 0; midiTxBuf[midiTxBufIndex] = data; if (midiTxBufIndex == 3 && ((midiTxBuf[0] ^ midiTxBuf[1] ^ midiTxBuf[2]) & 0x7f) == midiTxBuf[3]) { - if (midiTxBuf[0] == 0x84) - torque = ((midiTxBuf[1] << 7) | midiTxBuf[2]) - 0x80; - else if (midiTxBuf[0] == 0xff) - ffbCalibrating = true; - else if (midiTxBuf[0] == 0xf0) - ffbCalibrating = false; + const u8 cmd = midiTxBuf[0] & 0x7f; + switch (cmd) + { + case 0: + // FFB on/off + if (midiTxBuf[2] == 0) + { + active = false; + haptic::stopAll(0); + if (calibrating) { + calibrating = false; + os_notify("Calibration done", 2000); + } + } + else if (midiTxBuf[2] == 1) { + active = true; + haptic::setDamper(0, damperParam * power, damperSpeed); + } + break; - if (!ffbCalibrating) + // 01: 30 40 then 7f 40 (sgdrvsim search base pos) + // 30 40 (*2 initdv3e init, clubk after init) + // 7f 7f (kingrt init) + // 1f 1f kingrt test menu + // 02: 00 54 (sgdrvsim search base pos, kingrt) + // 7f 54 (*2 initdv3e init) + // 04 54 (clubk after init) + + case 3: + // Drive power + // buf[2]? initdv3, clubk2k3: 4, kingrt: 0, sgdrvsim: 28 + power = (midiTxBuf[1] >> 3) / 15.f; + break; + case 4: + // Torque + torque = ((midiTxBuf[1] << 7) | midiTxBuf[2]) - 0x80; + if (active && !calibrating) + haptic::setTorque(0, torque / 128.f * power); + break; + case 5: + // Rumble + // decoding from FFB Arcade Plugin (by Boomslangnz) + // https://github.com/Boomslangnz/FFBArcadePlugin/blob/master/Game%20Files/Demul.cpp + // buf[1]? + if (active) + MapleConfigMap::UpdateVibration(0, std::max(0.f, (float)(midiTxBuf[2] - 1) / 24.f * power), 0.f, 17); + break; + case 6: + // Damper + damperParam = midiTxBuf[1] / 127.f; + damperSpeed = midiTxBuf[2] / 127.f; + // clubkart sets it to 28 40 in menus, and 04 12 when in game (not changing in between) + // initdv3 starts with 02 2c // FIXME no effect with sat=2 + // changes it in-game: 02 11-2c + // finish line(?): 02 5a + // ends with 00 00 + // kingrt66: 60 0a (start), 40 0a (in game) + // sgdrvsim: 08 20 when calibrating center + // 18 40 init + // 28 nn in menus (nn is viscoSpeed<<6 >>8 from table: 20,28,30,38,...,98) + // 1e+n 0+m in game (n and m are set in test menu, default 1e, 0)) + // also: 8+n 0a+m and 0+n 3c+m + // byte1 is effect force? byte2 speed of effect? + if (active && !calibrating) + haptic::setDamper(0, damperParam * power, damperSpeed); + break; + + // 07 nn nn: set wheel center. n=004d 90° right, 0100 center, 011a ? left + // 09 00 00: kingrt init + // 03 40: end init + // 0A 10 58: kingrt end init + // 0B nn mm: auto center? deflection range? + // sgdrvsim: 20 10 in menus, else 00 00. Also nn 7f (nn computed) + // kingrt: 1b 00 end init + // 70 00 00: kingrt init (before 7f & 7a), kingrt test menu (after 7f) + // 7A 00 10,14: clubk,initv3e,kingrt init/tets menu + // 7C 00 3f: initdv3e init + // 20: clubk init + // 30: kingrt init + // 7D 00 00: nop, poll + + case 0x7f: + // FIXME also set when entering the service menu (kingrt66) + os_notify("Calibrating the wheel. Keep it centered.", 10000); + calibrating = true; + haptic::setSpring(0, 0.8f, 1.f); + position = 8192.f; + break; + } + + + if (!calibrating) { int direction = -1; if (NaomiGameInputs != nullptr) @@ -468,21 +553,107 @@ static void forceFeedbackMidiReceiver(u8 data) // required: b1 & 0x1f == 0x10 && b1 & 0x40 == 0 midiSend(0x90, ((int)position >> 7) & 0x7f, (int)position & 0x7f); - // decoding from FFB Arcade Plugin (by Boomslangnz) - // https://github.com/Boomslangnz/FFBArcadePlugin/blob/master/Game%20Files/Demul.cpp - if (midiTxBuf[0] == 0x85) - MapleConfigMap::UpdateVibration(0, std::max(0.f, (float)(midiTxBuf[2] - 1) / 24.f), 0.f, 5); - if (midiTxBuf[0] != 0xfd) + if (cmd != 0x7d) { networkOutput.output("midiffb", (midiTxBuf[0] << 16) | (midiTxBuf[1]) << 8 | midiTxBuf[2]); + DEBUG_LOG(NAOMI, "midiFFB: %02x %02x %02x", cmd, midiTxBuf[1], midiTxBuf[2]); + } } midiTxBufIndex = (midiTxBufIndex + 1) % std::size(midiTxBuf); } -void initMidiForceFeedback() +void init() { - aica::setMidiReceiver(forceFeedbackMidiReceiver); + aica::setMidiReceiver(midiReceiver); + initialized = true; + reset(); } +void reset() +{ + active = false; + calibrating = false; + midiTxBufIndex = 0; + power = 0.8f; + damperParam = 0.f; + damperSpeed = 0.f; + torque = 0.f; +} + +void term() +{ + aica::setMidiReceiver(nullptr); + initialized = false; +} + +void serialize(Serializer& ser) +{ + if (initialized) + { + ser << midiTxBuf; + ser << midiTxBufIndex; + ser << calibrating; + ser << active; + ser << power; + ser << damperParam; + ser << damperSpeed; + ser << position; + ser << torque; + } +} + +void deserialize(Deserializer& deser) +{ + if (deser.version() >= Deserializer::V27) + { + if (initialized) { + deser >> midiTxBuf; + deser >> midiTxBufIndex; + } + else if (deser.version() < Deserializer::V51) { + deser.skip(4); // midiTxBuf + deser.skip(); // midiTxBufIndex + } + } + else { + midiTxBufIndex = 0; + } + if (deser.version() >= Deserializer::V34) + { + if (initialized) + deser >> calibrating; + else if (deser.version() < Deserializer::V51) + deser.skip(); // calibrating + } + else { + calibrating = false; + } + if (initialized) + { + if (deser.version() >= Deserializer::V51) + { + deser >> active; + deser >> power; + deser >> damperParam; + deser >> damperSpeed; + deser >> position; + deser >> torque; + if (active && !calibrating) + haptic::setDamper(0, damperParam * power, damperSpeed); + } + else + { + active = false; + power = 0.8f; + damperParam = 0.f; + damperSpeed = 0.f; + position = 8192.f; + torque = 0.f; + } + } +} + +} // namespace midiffb + struct DriveSimPipe : public SerialPort::Pipe { void write(u8 data) override diff --git a/core/hw/naomi/naomi.h b/core/hw/naomi/naomi.h index f86bf31d3..d1f8c8c38 100644 --- a/core/hw/naomi/naomi.h +++ b/core/hw/naomi/naomi.h @@ -20,9 +20,18 @@ u16 NaomiGameIDRead(); void NaomiGameIDWrite(u16 data); void setGameSerialId(const u8 *data); -void initMidiForceFeedback(); void initDriveSimSerialPipe(); +namespace midiffb { + +void init(); +void reset(); +void term(); +void serialize(Serializer& ser); +void deserialize(Deserializer& deser); + +} + u32 libExtDevice_ReadMem_A0_006(u32 addr, u32 size); void libExtDevice_WriteMem_A0_006(u32 addr, u32 data, u32 size); diff --git a/core/hw/naomi/naomi_cart.cpp b/core/hw/naomi/naomi_cart.cpp index 588def40e..4b70e1899 100644 --- a/core/hw/naomi/naomi_cart.cpp +++ b/core/hw/naomi/naomi_cart.cpp @@ -654,7 +654,7 @@ void naomi_cart_LoadRom(const std::string& path, const std::string& fileName, Lo || gameId == "INITIAL D CYCRAFT") { card_reader::initdInit(); - initMidiForceFeedback(); + midiffb::init(); } else if (gameId == "MAXIMUM SPEED" || gameId == "FASTER THAN SPEED") { @@ -665,7 +665,7 @@ void naomi_cart_LoadRom(const std::string& path, const std::string& fileName, Lo || gameId == "SEGA DRIVING SIMULATOR") { if (settings.naomi.drivingSimSlave == 0) - initMidiForceFeedback(); + midiffb::init(); if (romName == "clubkrt" || romName == "clubkrto" || romName == "clubkrta" || romName == "clubkrtc") card_reader::clubkInit(); diff --git a/core/input/haptic.h b/core/input/haptic.h new file mode 100644 index 000000000..230728567 --- /dev/null +++ b/core/input/haptic.h @@ -0,0 +1,55 @@ +/* + Copyright 2024 flyinghead + + This file is part of Flycast. + + Flycast is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Flycast is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Flycast. If not, see . +*/ +#pragma once +#include "build.h" + +#ifdef USE_SDL +void sdl_setTorque(int, float); +void sdl_setDamper(int, float, float); +void sdl_setSpring(int, float, float); +void sdl_stopHaptic(int); +#endif + +namespace haptic { + +inline static void setTorque(int port, float v) { +#ifdef USE_SDL + sdl_setTorque(port, v); +#endif +} + +inline static void setDamper(int port, float param, float speed) { +#ifdef USE_SDL + sdl_setDamper(port, param, speed); +#endif +} + +inline static void setSpring(int port, float saturation, float speed) { +#ifdef USE_SDL + sdl_setSpring(port, saturation, speed); +#endif +} + +inline static void stopAll(int port) { +#ifdef USE_SDL + sdl_stopHaptic(port); +#endif +} + +} diff --git a/core/sdl/sdl.cpp b/core/sdl/sdl.cpp index 48b8259f7..215843642 100644 --- a/core/sdl/sdl.cpp +++ b/core/sdl/sdl.cpp @@ -51,6 +51,9 @@ static u64 lastBarcodeTime; static KeyboardLayout detectKeyboardLayout(); static bool handleBarcodeScanner(const SDL_Event& event); +void sdl_stopHaptic(int port); +static void pauseHaptic(); +static void resumeHaptic(); static struct SDLDeInit { @@ -64,6 +67,8 @@ static struct SDLDeInit static void sdl_open_joystick(int index) { + if (settings.naomi.slave) + return; SDL_Joystick *pJoystick = SDL_JoystickOpen(index); if (pJoystick == NULL) @@ -84,6 +89,8 @@ static void sdl_open_joystick(int index) static void sdl_close_joystick(SDL_JoystickID instance) { + if (settings.naomi.slave) + return; std::shared_ptr gamepad = SDLGamepad::GetSDLGamepad(instance); if (gamepad != NULL) gamepad->close(); @@ -129,6 +136,7 @@ static void emuEventCallback(Event event, void *) { case Event::Terminate: SDL_SetWindowTitle(window, "Flycast"); + sdl_stopHaptic(0); break; case Event::Pause: gameRunning = false; @@ -136,13 +144,14 @@ static void emuEventCallback(Event event, void *) SDL_SetRelativeMouseMode(SDL_FALSE); SDL_ShowCursor(SDL_ENABLE); setWindowTitleGame(); + pauseHaptic(); break; case Event::Resume: gameRunning = true; captureMouse(mouseCaptured); if (window_fullscreen && !mouseCaptured) SDL_ShowCursor(SDL_DISABLE); - + resumeHaptic(); break; default: break; @@ -212,6 +221,8 @@ void input_sdl_init() die("SDL: error initializing Joystick subsystem"); } sdlDeInit.initialized = true; + if (SDL_WasInit(SDL_INIT_HAPTIC) == 0) + SDL_InitSubSystem(SDL_INIT_HAPTIC); SDL_SetRelativeMouseMode(SDL_FALSE); @@ -257,10 +268,11 @@ void input_sdl_quit() EventManager::unlisten(Event::Pause, emuEventCallback); EventManager::unlisten(Event::Resume, emuEventCallback); SDLGamepad::closeAllGamepads(); - SDL_QuitSubSystem(SDL_INIT_JOYSTICK); + SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC); } -inline void SDLMouse::setAbsPos(int x, int y) { +inline void SDLMouse::setAbsPos(int x, int y) +{ int width, height; SDL_GetWindowSize(window, &width, &height); if (width != 0 && height != 0) @@ -1203,3 +1215,86 @@ static bool handleBarcodeScanner(const SDL_Event& event) return true; } + +static float torque; +static float springSat; +static float springSpeed; +static float damperParam; +static float damperSpeed; + +void sdl_setTorque(int port, float torque) +{ + ::torque = torque; + if (gameRunning) + SDLGamepad::SetTorque(port, torque); +} + +void sdl_setSpring(int port, float saturation, float speed) +{ + springSat = saturation; + springSpeed = speed; + SDLGamepad::SetSpring(port, saturation, speed); +} + +void sdl_setDamper(int port, float param, float speed) +{ + damperParam = param; + damperSpeed = speed; + SDLGamepad::SetDamper(port, param, speed); +} + +void sdl_stopHaptic(int port) +{ + torque = 0.f; + springSat = 0.f; + springSpeed = 0.f; + damperParam = 0.f; + damperSpeed = 0.f; + SDLGamepad::StopHaptic(port); +} + +void pauseHaptic() { + SDLGamepad::SetTorque(0, 0.f); +} + +void resumeHaptic() { + SDLGamepad::SetTorque(0, torque); +} + +#if 0 +#include "ui/gui_util.h" + +void sdl_displayHapticStats() +{ + ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0); + ImguiStyleVar _1(ImGuiStyleVar_WindowBorderSize, 0); + ImGui::SetNextWindowPos(ImVec2(10, 10)); + ImGui::SetNextWindowSize(ScaledVec2(120, 0)); + ImGui::SetNextWindowBgAlpha(0.7f); + ImGui::Begin("##ggpostats", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs); + ImguiStyleColor _2(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f)); + + ImGui::Text("Torque"); + char s[32]; + snprintf(s, sizeof(s), "%.1f", torque); + ImGui::ProgressBar(0.5f + torque / 2.f, ImVec2(-1, 0), s); + + ImGui::Text("Spring Sat"); + snprintf(s, sizeof(s), "%.1f", springSat); + ImGui::ProgressBar(springSat, ImVec2(-1, 0), s); + + ImGui::Text("Spring Speed"); + snprintf(s, sizeof(s), "%.1f", springSpeed); + ImGui::ProgressBar(springSpeed, ImVec2(-1, 0), s); + + ImGui::Text("Damper Param"); + snprintf(s, sizeof(s), "%.1f", damperParam); + ImGui::ProgressBar(damperParam, ImVec2(-1, 0), s); + + ImGui::Text("Damper Speed"); + snprintf(s, sizeof(s), "%.1f", damperSpeed); + ImGui::ProgressBar(damperSpeed, ImVec2(-1, 0), s); + + ImGui::End(); +} +#endif diff --git a/core/sdl/sdl_gamepad.h b/core/sdl/sdl_gamepad.h index 60782f738..89d4b26c5 100644 --- a/core/sdl/sdl_gamepad.h +++ b/core/sdl/sdl_gamepad.h @@ -176,7 +176,7 @@ public: _name = joyName; sdl_joystick_instance = SDL_JoystickInstanceID(sdl_joystick); _unique_id = "sdl_joystick_" + std::to_string(sdl_joystick_instance); - INFO_LOG(INPUT, "SDL: Opened joystick %d on port %d: '%s' unique_id=%s", sdl_joystick_instance, maple_port, _name.c_str(), _unique_id.c_str()); + NOTICE_LOG(INPUT, "SDL: Opened joystick %d on port %d: '%s' unique_id=%s", sdl_joystick_instance, maple_port, _name.c_str(), _unique_id.c_str()); if (SDL_IsGameController(joystick_idx)) { @@ -201,14 +201,96 @@ public: else INFO_LOG(INPUT, "using custom mapping '%s'", input_mapper->name.c_str()); + hasAnalogStick = SDL_JoystickNumAxes(sdl_joystick) > 0; + set_maple_port(maple_port); + #if SDL_VERSION_ATLEAST(2, 0, 18) rumbleEnabled = SDL_JoystickHasRumble(sdl_joystick); #else rumbleEnabled = (SDL_JoystickRumble(sdl_joystick, 1, 1, 1) != -1); #endif - hasAnalogStick = SDL_JoystickNumAxes(sdl_joystick) > 0; - set_maple_port(maple_port); + if (SDL_JoystickGetType(sdl_joystick) == SDL_JOYSTICK_TYPE_WHEEL) + { + // Open the haptic interface + haptic = SDL_HapticOpenFromJoystick(sdl_joystick); + if (haptic != nullptr) + { + // Query supported haptic effects for force-feedback + u32 hapq = SDL_HapticQuery(haptic); + INFO_LOG(INPUT, "SDL_HapticQuery: supported: %x", hapq); + if (hapq & SDL_HAPTIC_SINE) + { + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_SINE; + effect.periodic.direction.type = SDL_HAPTIC_CARTESIAN; + effect.periodic.direction.dir[0] = -1; // west + effect.periodic.period = 40; // 25 Hz + effect.periodic.magnitude = 0x7fff; + effect.periodic.length = SDL_HAPTIC_INFINITY; + sineEffectId = SDL_HapticNewEffect(haptic, &effect); + if (sineEffectId != -1) + { + rumbleEnabled = true; + hapticRumble = true; + NOTICE_LOG(INPUT, "wheel %d: haptic sine supported", sdl_joystick_instance); + } + } + if (hapq & SDL_HAPTIC_AUTOCENTER) + { + SDL_HapticSetAutocenter(haptic, 0); + hasAutocenter = true; + NOTICE_LOG(INPUT, "wheel %d: haptic autocenter supported", sdl_joystick_instance); + } + if (hapq & SDL_HAPTIC_GAIN) + SDL_HapticSetGain(haptic, 100); + if (hapq & SDL_HAPTIC_CONSTANT) + { + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_CONSTANT; + effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; + effect.constant.direction.dir[0] = -1; // west, updated when used + effect.constant.length = SDL_HAPTIC_INFINITY; + effect.constant.delay = 0; + effect.constant.level = 0; // updated when used + constEffectId = SDL_HapticNewEffect(haptic, &effect); + if (constEffectId != -1) + NOTICE_LOG(INPUT, "wheel %d: haptic constant supported", sdl_joystick_instance); + } + if (hapq & SDL_HAPTIC_SPRING) + { + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_SPRING; + effect.condition.length = SDL_HAPTIC_INFINITY; + effect.condition.direction.type = SDL_HAPTIC_CARTESIAN; // not used but required! + // effect level at full deflection + effect.condition.left_sat[0] = effect.condition.right_sat[0] = 0xffff; + // how fast to increase the force + effect.condition.left_coeff[0] = effect.condition.right_coeff[0] = 0x7fff; + springEffectId = SDL_HapticNewEffect(haptic, &effect); + if (springEffectId != -1) + NOTICE_LOG(INPUT, "wheel %d: haptic spring supported", sdl_joystick_instance); + } + if (hapq & SDL_HAPTIC_DAMPER) + { + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_DAMPER; + effect.condition.length = SDL_HAPTIC_INFINITY; + effect.condition.direction.type = SDL_HAPTIC_CARTESIAN; // not used but required! + // max effect level + effect.condition.left_sat[0] = effect.condition.right_sat[0] = 0xffff; + // how fast to increase the force + effect.condition.left_coeff[0] = effect.condition.right_coeff[0] = 0x7fff; + damperEffectId = SDL_HapticNewEffect(haptic, &effect); + if (damperEffectId != -1) + NOTICE_LOG(INPUT, "wheel %d: haptic damper supported", sdl_joystick_instance); + } + if (sineEffectId == -1 && constEffectId == -1 && damperEffectId == -1 && springEffectId == -1) { + SDL_HapticClose(haptic); + haptic = nullptr; + } + } + } } bool gamepad_axis_input(u32 code, int value) override @@ -227,6 +309,25 @@ public: u16 getRumbleIntensity(float power) { return (u16)std::min(power * 65535.f / std::pow(1.06f, 100.f - rumblePower), 65535.f); } + void doRumble(float power, u32 duration_ms) + { + const u16 intensity = getRumbleIntensity(power); + if (hapticRumble) + { + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_SINE; + effect.periodic.direction.type = SDL_HAPTIC_CARTESIAN; + effect.periodic.direction.dir[0] = (vib_stop_time & 1) ? -1 : 1; // west or east randomly + effect.periodic.period = 40; // 25 Hz + effect.periodic.magnitude = intensity / 4; // scale by an additional 0.5 to soften it + effect.periodic.length = duration_ms; + SDL_HapticUpdateEffect(haptic, sineEffectId, &effect); + SDL_HapticRunEffect(haptic, sineEffectId, 1); + } + else { + SDL_JoystickRumble(sdl_joystick, intensity, intensity, duration_ms); + } + } void rumble(float power, float inclination, u32 duration_ms) override { @@ -234,9 +335,7 @@ public: { vib_inclination = inclination * power; vib_stop_time = getTimeMs() + duration_ms; - - u16 intensity = getRumbleIntensity(power); - SDL_JoystickRumble(sdl_joystick, intensity, intensity, duration_ms); + doRumble(power, duration_ms); } } void update_rumble() override @@ -247,18 +346,111 @@ public: { int rem_time = vib_stop_time - getTimeMs(); if (rem_time <= 0) - vib_inclination = 0; - else { - u16 intensity = getRumbleIntensity(vib_inclination * rem_time); - SDL_JoystickRumble(sdl_joystick, intensity, intensity, rem_time); + vib_inclination = 0; + if (hapticRumble) + SDL_HapticStopEffect(haptic, sineEffectId); + else + SDL_JoystickRumble(sdl_joystick, 0, 0, 0); + } + else { + doRumble(vib_inclination * rem_time, rem_time); } } } + void setTorque(float torque) + { + if (haptic == nullptr || constEffectId == -1) + return; + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_CONSTANT; + effect.constant.direction.type = SDL_HAPTIC_CARTESIAN; + effect.constant.direction.dir[0] = torque < 0 ? -1 : 1; // west/cw if torque < 0 + effect.constant.length = SDL_HAPTIC_INFINITY; + effect.constant.level = std::abs(torque) * 32767.f * rumblePower / 100.f; + SDL_HapticUpdateEffect(haptic, constEffectId, &effect); + SDL_HapticRunEffect(haptic, constEffectId, 1); + } + + void stopHaptic() + { + if (haptic != nullptr) + { + SDL_HapticStopAll(haptic); + if (hasAutocenter) + SDL_HapticSetAutocenter(haptic, 0); + } + } + + void setSpring(float saturation, float speed) + { + if (haptic == nullptr) + return; + if (springEffectId == -1) { + // Spring not supported so use autocenter if available + if (hasAutocenter) + SDL_HapticSetAutocenter(haptic, saturation * rumblePower); + } + else + { + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_SPRING; + effect.condition.length = SDL_HAPTIC_INFINITY; + effect.condition.direction.type = SDL_HAPTIC_CARTESIAN; + // effect level at full deflection + effect.condition.left_sat[0] = effect.condition.right_sat[0] = (saturation * rumblePower / 100.f) * 0xffff; + // how fast to increase the force + effect.condition.left_coeff[0] = effect.condition.right_coeff[0] = speed * 0x7fff; + SDL_HapticUpdateEffect(haptic, springEffectId, &effect); + SDL_HapticRunEffect(haptic, springEffectId, 1); + } + } + + void setDamper(float param, float speed) + { + if (haptic == nullptr || damperEffectId == -1) + return; + SDL_HapticEffect effect{}; + effect.type = SDL_HAPTIC_DAMPER; + effect.condition.length = SDL_HAPTIC_INFINITY; + effect.condition.direction.type = SDL_HAPTIC_CARTESIAN; + // max effect level + effect.condition.left_sat[0] = effect.condition.right_sat[0] = (param * rumblePower / 100.f) * 0xffff; + // how fast to increase the force + effect.condition.left_coeff[0] = effect.condition.right_coeff[0] = speed * 0x7fff; + SDL_HapticUpdateEffect(haptic, damperEffectId, &effect); + SDL_HapticRunEffect(haptic, damperEffectId, 1); + } + void close() { - INFO_LOG(INPUT, "SDL: Joystick '%s' on port %d disconnected", _name.c_str(), maple_port()); + NOTICE_LOG(INPUT, "SDL: Joystick '%s' on port %d disconnected", _name.c_str(), maple_port()); + if (haptic != nullptr) + { + stopHaptic(); + SDL_HapticSetGain(haptic, 0); + if (sineEffectId != -1) { + SDL_HapticDestroyEffect(haptic, sineEffectId); + sineEffectId = -1; + } + if (constEffectId != -1) { + SDL_HapticDestroyEffect(haptic, constEffectId); + constEffectId = -1; + } + if (springEffectId != -1) { + SDL_HapticDestroyEffect(haptic, springEffectId); + springEffectId = -1; + } + if (damperEffectId != -1) { + SDL_HapticDestroyEffect(haptic, damperEffectId); + damperEffectId = -1; + } + SDL_HapticClose(haptic); + haptic = nullptr; + rumbleEnabled = false; + hapticRumble = false; + } if (sdl_controller != nullptr) SDL_GameControllerClose(sdl_controller); SDL_JoystickClose(sdl_joystick); @@ -417,6 +609,27 @@ public: pair.second->update_rumble(); } + static void SetTorque(int port, float torque) { + for (auto& pair : sdl_gamepads) + if (pair.second->maple_port() == port) + pair.second->setTorque(torque); + } + static void SetSpring(int port, float saturation, float speed) { + for (auto& pair : sdl_gamepads) + if (pair.second->maple_port() == port) + pair.second->setSpring(saturation, speed); + } + static void SetDamper(int port, float param, float speed) { + for (auto& pair : sdl_gamepads) + if (pair.second->maple_port() == port) + pair.second->setDamper(param, speed); + } + static void StopHaptic(int port) { + for (auto& pair : sdl_gamepads) + if (pair.second->maple_port() == port) + pair.second->stopHaptic(); + } + protected: u64 vib_stop_time = 0; SDL_JoystickID sdl_joystick_instance; @@ -426,6 +639,13 @@ private: float vib_inclination = 0; SDL_GameController *sdl_controller = nullptr; static std::map> sdl_gamepads; + SDL_Haptic *haptic = nullptr; + bool hapticRumble = false; + bool hasAutocenter = false; + int sineEffectId = -1; + int constEffectId = -1; + int springEffectId = -1; + int damperEffectId = -1; }; std::map> SDLGamepad::sdl_gamepads; diff --git a/core/serialize.h b/core/serialize.h index 5650122d0..cc397c68c 100644 --- a/core/serialize.h +++ b/core/serialize.h @@ -61,7 +61,8 @@ public: V48, V49, V50, - Current = V50, + V51, + Current = V51, Next = Current + 1, }; diff --git a/tests/src/serialize_test.cpp b/tests/src/serialize_test.cpp index 168699ce6..75a769128 100644 --- a/tests/src/serialize_test.cpp +++ b/tests/src/serialize_test.cpp @@ -32,7 +32,7 @@ TEST_F(SerializeTest, SizeTest) std::vector data(30000000); Serializer ser(data.data(), data.size()); dc_serialize(ser); - ASSERT_EQ(28191443u, ser.size()); + ASSERT_EQ(28191434u, ser.size()); }