haptic: add sine effect. implement spring for midi FFB

Add sine effect to haptic api and stop using maple rumble for naomi FFB.
Emulate spring in midi FFB. Thanks to njz3 for his help on decoding the
protocol.
Don't use exponential scale for rumble when using haptic sine effect.

serialize Sh4OCache::writeBack/ThroughBufferCycles
This commit is contained in:
Flyinghead 2025-06-16 17:50:36 +02:00
parent 71341a27e4
commit 8801b2ae16
8 changed files with 147 additions and 41 deletions

View File

@ -825,7 +825,7 @@ protected:
{
// 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?
haptic::setSine(0, v, 25.f, 650); // 25 Hz, 650 ms
}
else if (in >= 0xe0 && in <= 0xef)
{

View File

@ -18,7 +18,6 @@
#include "hw/holly/sb.h"
#include "hw/sh4/sh4_mem.h"
#include "hw/holly/holly_intc.h"
#include "hw/maple/maple_cfg.h"
#include "hw/sh4/sh4_sched.h"
#include "hw/aica/aica_if.h"
#include "hw/hwreg.h"
@ -454,6 +453,8 @@ static float power = 0.8f;
static bool active;
static float position = 8192.f;
static float torque;
static float springForce;
static u8 maxSpring = 0x7f;
static void midiSend(u8 b1, u8 b2, u8 b3)
{
@ -463,6 +464,7 @@ static void midiSend(u8 b1, u8 b2, u8 b3)
aica::midiSend((b1 ^ b2 ^ b3) & 0x7f);
}
// Thanks to njz3 and boomslangnz for their help
// https://www.arcade-projects.com/threads/force-feedback-translator-sega-midi-sega-rs422-and-namco-rs232.924/
static void midiReceiver(u8 data)
{
@ -478,6 +480,8 @@ static void midiReceiver(u8 data)
{
case 0:
// FFB on/off
// b1 & 1 => Temporary (a few 10th of a second) else permanently
// b2 & 1 => FFB enabled
if (midiTxBuf[2] == 0)
{
active = false;
@ -487,43 +491,75 @@ static void midiReceiver(u8 data)
os_notify("Calibration done", 2000);
}
}
else if (midiTxBuf[2] == 1) {
else if (midiTxBuf[2] == 1)
{
active = true;
haptic::setDamper(0, damperParam * power, damperSpeed);
haptic::setDamper(0, damperSpeed * power, damperParam);
haptic::setSpring(0, springForce * power, 1.f);
}
break;
// 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 1:
// Set force
// b1: max torque forward (centering force)
// b2: max torque backward (anti-centering force or friction)
// Ex: 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
maxSpring = midiTxBuf[1];
break;
case 2:
// Unknown, seems to be used to enable something, usually follows 1 or 6
// b1: 00 or 04 or 1F or 7F
// b2: 54
// Ex: 00 54 (sgdrvsim search base pos, kingrt)
// 7f 54 (*2 initdv3e init)
// 04 54 (clubk after init)
break;
case 3:
// Drive power
// buf[2]? initdv3, clubk2k3: 4, kingrt: 0, sgdrvsim: 28
// b1: power level [0-7f]
// b2: ? initdv3, clubk2k3: 4, kingrt: 0, sgdrvsim: 28
power = (midiTxBuf[1] >> 3) / 15.f;
break;
case 4:
// Torque
// from 0 (max torque to the right) to 17f (max torque to the left)
torque = ((midiTxBuf[1] << 7) | midiTxBuf[2]) - 0x80;
if (active && !calibrating)
haptic::setTorque(0, torque / 128.f * power);
break;
case 5:
// Rumble
// b1: frequency in half Hz
// b2: amplitude
// Examples:
// * 02 40: large sine of 1Hz
// * 0A 20: smaller sine of 5 Hz
// 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);
{
const float intensity = std::clamp((float)(midiTxBuf[2] - 1) / 80.f * power, 0.f, 1.f);
const float freq = midiTxBuf[1] / 2.f;
haptic::setSine(0, intensity, freq, 1000);
}
break;
case 6:
// Damper
damperParam = midiTxBuf[1] / 127.f;
damperSpeed = midiTxBuf[2] / 127.f;
// Damper effect expressed as a ratio param1/param2, or a pole of
// a transfer function.
// Examples:
// * 10 7F: strong damper
// * 40 40: light damper
// * XX 00: disabled
damperSpeed = midiTxBuf[1] / 127.f;
damperParam = 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
@ -535,26 +571,47 @@ static void midiReceiver(u8 data)
// 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);
haptic::setDamper(0, damperSpeed * power, damperParam);
break;
// 07 nn nn: set wheel center. n=004d 90° right, 0100 center, 011a ? left
// 07 nn nn: Spring angle offset from center.
// b1: 00 right, 01 left
// b2: offset [0-7f] max 90 deg
// 08: Spring effect inverted gain or limit?
// 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
case 0xB:
// Spring force
// b1: force
// b2: 00
// sgdrvsim: 20 10 in menus, else 00 00. Also [0-7f] 7f
// kingrt: 1b 00 (boot)
// clubk2k: 00 00 (boot) 20 10 (menus) 00 00 (drive)
springForce = std::min(midiTxBuf[1], maxSpring) / 127.f;
if (active && !calibrating)
haptic::setSpring(0, springForce * power, 1.f);
break;
// 70 00 00: Set wheel center
// kingrt init (before 7f & 7a), kingrt test menu (after 7f)
// 7A 00 nn: Set reply mode
// 10: long status
// 14: long status + encoder feedback (club2k, drvsimm)
// 1f: short status
// 00 10,14: clubk,initv3e,kingrt init/test menu
// 7C 00 nn Start phase alignment. From 0 (disabled) to 7f (very strong)
// 00 3f: initdv3e init
// 20: clubk init
// 30: kingrt init
// 7D 00 00: nop, poll
// 7D 00 00: Query status
case 0x7f:
// Reset board
// FIXME also set when entering the service menu (kingrt66)
os_notify("Calibrating the wheel. Keep it centered.", 10000);
calibrating = true;
@ -620,6 +677,8 @@ void serialize(Serializer& ser)
ser << damperSpeed;
ser << position;
ser << torque;
ser << maxSpring;
ser << springForce;
}
}
@ -651,6 +710,8 @@ void deserialize(Deserializer& deser)
}
if (initialized)
{
maxSpring = 0x7f;
springForce = 0;
if (deser.version() >= Deserializer::V51)
{
deser >> active;
@ -659,8 +720,14 @@ void deserialize(Deserializer& deser)
deser >> damperSpeed;
deser >> position;
deser >> torque;
if (active && !calibrating)
haptic::setDamper(0, damperParam * power, damperSpeed);
if (deser.version() >= Deserializer::V55) {
deser >> maxSpring;
deser >> springForce;
}
if (active && !calibrating) {
haptic::setDamper(0, damperSpeed * power, damperParam);
haptic::setSpring(0, springForce * power, 1.f);
}
}
else
{

View File

@ -385,11 +385,19 @@ public:
memset(&lines[0], 0, sizeof(lines));
}
void Serialize(Serializer& ser) {
void Serialize(Serializer& ser)
{
ser << lines;
ser << writeBackBufferCycles;
ser << writeThroughBufferCycles;
}
void Deserialize(Deserializer& deser) {
void Deserialize(Deserializer& deser)
{
deser >> lines;
if (deser.version() >= Serializer::V55) {
deser >> writeBackBufferCycles;
deser >> writeThroughBufferCycles;
}
}
u32 ReadAddressArray(u32 addr)
@ -598,7 +606,6 @@ private:
}
std::array<cache_line, 512> lines;
// TODO serialize
u64 writeBackBufferCycles = 0;
u64 writeThroughBufferCycles = 0;
Sh4Cycles sh4cycles;

View File

@ -23,6 +23,7 @@
void sdl_setTorque(int, float);
void sdl_setDamper(int, float, float);
void sdl_setSpring(int, float, float);
void sdl_setSine(int, float, float, u32);
void sdl_stopHaptic(int);
#endif
@ -46,6 +47,12 @@ inline static void setSpring(int port, float saturation, float speed) {
#endif
}
inline static void setSine(int port, float power, float frequency, u32 duration_ms) {
#ifdef USE_SDL
sdl_setSine(port, power, frequency, duration_ms);
#endif
}
inline static void stopAll(int port) {
#ifdef USE_SDL
sdl_stopHaptic(port);

View File

@ -1259,6 +1259,8 @@ static float springSat;
static float springSpeed;
static float damperParam;
static float damperSpeed;
static float rumblePower;
static float rumbleFreq;
void sdl_setTorque(int port, float torque)
{
@ -1281,6 +1283,13 @@ void sdl_setDamper(int port, float param, float speed)
SDLGamepad::SetDamper(port, param, speed);
}
void sdl_setSine(int port, float power, float freq, u32 duration_ms)
{
rumblePower = power;
rumbleFreq = freq;
SDLGamepad::SetSine(port, power, freq, duration_ms);
}
void sdl_stopHaptic(int port)
{
torque = 0.f;
@ -1315,7 +1324,7 @@ void sdl_displayHapticStats()
ImGui::Text("Torque");
char s[32];
snprintf(s, sizeof(s), "%.1f", torque);
ImGui::ProgressBar(0.5f + torque / 2.f, ImVec2(-1, 0), s);
ImGui::ProgressBar(0.5f - torque / 2.f, ImVec2(-1, 0), s);
ImGui::Text("Spring Sat");
snprintf(s, sizeof(s), "%.1f", springSat);
@ -1333,6 +1342,12 @@ void sdl_displayHapticStats()
snprintf(s, sizeof(s), "%.1f", damperSpeed);
ImGui::ProgressBar(damperSpeed, ImVec2(-1, 0), s);
ImGui::Text("Rumble");
snprintf(s, sizeof(s), "%.1f", rumblePower);
ImGui::ProgressBar(rumblePower, ImVec2(-1, 0), s);
snprintf(s, sizeof(s), "%.0f Hz", rumbleFreq);
ImGui::ProgressBar(rumbleFreq / 200.f, ImVec2(-1, 0), s);
ImGui::End();
}
#endif

View File

@ -310,20 +310,25 @@ public:
else
return (u16)std::min(power * 65535.f / std::pow(1.06f, 100.f - rumblePower), 65535.f);
}
void doRumble(float power, u32 duration_ms)
void doRumble(float power, u32 duration_ms) {
setSine(power, 25.f, duration_ms);
}
void setSine(float power, float freq, u32 duration_ms)
{
const u16 intensity = getRumbleIntensity(power);
if (!rumbleEnabled)
return;
if (hapticRumble)
{
if (intensity != 0 && duration_ms != 0)
if (power != 0.f && freq != 0.f && duration_ms != 0)
{
SDL_HapticEffect effect{};
effect.type = SDL_HAPTIC_SINE;
effect.periodic.direction.type = SDL_HAPTIC_STEERING_AXIS;
effect.periodic.direction.dir[0] = 1;
effect.periodic.period = 40; // 25 Hz
// scale by an additional 0.5 to soften it and pick random direction
effect.periodic.magnitude = intensity / 4 * ((rand() & 1) * 2 - 1);
effect.periodic.period = 1000 / std::min(freq, 100.f); // period in ms
// pick random direction
effect.periodic.magnitude = power * rumblePower / 100.f * 32767.f * ((rand() & 1) * 2 - 1);
effect.periodic.length = duration_ms;
SDL_HapticUpdateEffect(haptic, sineEffectId, &effect);
SDL_HapticRunEffect(haptic, sineEffectId, 1);
@ -333,6 +338,7 @@ public:
}
}
else {
const u16 intensity = getRumbleIntensity(power);
SDL_JoystickRumble(sdl_joystick, intensity, intensity, duration_ms);
}
}
@ -673,6 +679,9 @@ public:
static void SetDamper(int port, float param, float speed) {
applyToPort(port, &SDLGamepad::setDamper, param, speed);
}
static void SetSine(int port, float power, float freq, u32 duration_ms) {
applyToPort(port, &SDLGamepad::setSine, power, freq, duration_ms);
}
static void StopHaptic(int port) {
applyToPort(port, &SDLGamepad::stopHaptic);
}

View File

@ -65,7 +65,8 @@ public:
V52,
V53,
V54,
Current = V54,
V55,
Current = V55,
Next = Current + 1,
};

View File

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