input: ffb support for arcade games. rumble support (f355, 18wheeler)

SDL force feedback for racing wheels.
Support for jvs ffb (f355, 18wheeler), atomiswave (maxspeed, ftspeed)
and midi ffb (initd, clubk, kingrt66, sgdrvsim).
Simple haptic rumble for f355 and 18wheeler.
This commit is contained in:
Flyinghead 2024-06-01 11:03:14 +02:00
parent a5608f4f22
commit adeba60ba9
9 changed files with 731 additions and 68 deletions

View File

@ -24,6 +24,7 @@
#include "cfg/option.h"
#include "network/output.h"
#include "hw/naomi/printer.h"
#include "input/haptic.h"
#include <algorithm>
#include <array>
@ -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

View File

@ -33,6 +33,7 @@
#include "oslib/oslib.h"
#include "printer.h"
#include "hw/flashrom/x76f100.h"
#include "input/haptic.h"
#include <algorithm>
@ -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<u32>(); // midiTxBufIndex
}
}
else {
midiTxBufIndex = 0;
}
if (deser.version() >= Deserializer::V34)
{
if (initialized)
deser >> calibrating;
else if (deser.version() < Deserializer::V51)
deser.skip<bool>(); // 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

View File

@ -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);

View File

@ -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();

55
core/input/haptic.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#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
}
}

View File

@ -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<SDLGamepad> 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

View File

@ -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_JoystickID, std::shared_ptr<SDLGamepad>> 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<SDL_JoystickID, std::shared_ptr<SDLGamepad>> SDLGamepad::sdl_gamepads;

View File

@ -61,7 +61,8 @@ public:
V48,
V49,
V50,
Current = V50,
V51,
Current = V51,
Next = Current + 1,
};

View File

@ -32,7 +32,7 @@ TEST_F(SerializeTest, SizeTest)
std::vector<char> data(30000000);
Serializer ser(data.data(), data.size());
dc_serialize(ser);
ASSERT_EQ(28191443u, ser.size());
ASSERT_EQ(28191434u, ser.size());
}