From aa19d9cdf55a78236628cc3a4c302868f150aded Mon Sep 17 00:00:00 2001 From: General4878 Date: Sun, 10 Aug 2025 13:11:09 +0200 Subject: [PATCH 01/11] FLM: Created MoveByFauxLizard method which mimmicks the mouse movement and haptic feedback of Lizard Mouse without needing Lizard Mouse enabled which resolves the mouse stutter bug in later versions Steam Deck firmware. --- SteamController/Devices/MouseController.cs | 2 +- .../Devices/MouseControllerFauxLizard.cs | 177 ++++++++++++++++++ .../Profiles/Default/GuideShortcutsProfile.cs | 12 +- 3 files changed, 183 insertions(+), 8 deletions(-) create mode 100644 SteamController/Devices/MouseControllerFauxLizard.cs diff --git a/SteamController/Devices/MouseController.cs b/SteamController/Devices/MouseController.cs index 34a593e..4431d82 100644 --- a/SteamController/Devices/MouseController.cs +++ b/SteamController/Devices/MouseController.cs @@ -2,7 +2,7 @@ using WindowsInput; namespace SteamController.Devices { - public class MouseController : IDisposable + public partial class MouseController : IDisposable { private struct Accum { diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs new file mode 100644 index 0000000..8a6b99b --- /dev/null +++ b/SteamController/Devices/MouseControllerFauxLizard.cs @@ -0,0 +1,177 @@ +using static SteamController.Devices.SteamController; + +namespace SteamController.Devices +{ + public partial class MouseController : IDisposable + { + // Adjustable parameters (tweakable settings) + private const int bufferSize = 4; // Input smoothing buffer size + private const int gestureRadius = 30; // Gesture commit threshold (pixels) + private const double gestureFlushRate = 0.1; // Rate at which gesture flush decays + private const int velocityWindowSize = 20; // Velocity smoothing window size + private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide + private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide + private const double hapticTriggerDelta = 45; // Distance threshold for haptic feedback + private const sbyte hapticStrength = 5; // Haptic feedback intensity + + // Functional constants (derived once) + private readonly double gestureRadiusSq = gestureRadius * gestureRadius; + private readonly double minGlideMagnitudeSq = minGlideMagnitude * minGlideMagnitude; + private readonly double minGlideVelocitySq = minGlideVelocity * minGlideVelocity; + + // Runtime state + private Queue bufX = new(), bufY = new(); + private double totalDeltaX = 0, totalDeltaY = 0; + private bool gestureCommitted = false; + private double gestureFlushX = 0, gestureFlushY = 0; + + private bool isGliding = false; + private DateTime releaseTime; + private double releaseVelocityX = 0, releaseVelocityY = 0; + private Queue velocityHistoryX = new(), velocityHistoryY = new(); + private double velocitySumX = 0, velocitySumY = 0; + + private double hapticDelta = 0; + + // Main movement logic + public void MoveByFauxLizard(double dx, double dy, Context c) + { + bool isTouched = c.Steam.BtnRPadTouch?.LastValue ?? false; + + if (!isTouched) + { + // Start glide if gesture was committed and velocity is high enough + if (!isGliding && gestureCommitted) + { + double magSq = releaseVelocityX * releaseVelocityX + releaseVelocityY * releaseVelocityY; + if (magSq >= minGlideMagnitudeSq) + { + releaseTime = DateTime.Now; + isGliding = true; + } + } + + // Continue glide if active + if (isGliding) + { + double elapsed = (DateTime.Now - releaseTime).TotalSeconds; + double glideX = ApplyReleaseGlide(releaseVelocityX, elapsed); + double glideY = ApplyReleaseGlide(releaseVelocityY, elapsed); + + double glideMagSq = glideX * glideX + glideY * glideY; + if (glideMagSq < minGlideVelocitySq) + { + isGliding = false; + releaseVelocityX = releaseVelocityY = 0; + } + else + { + MoveBy(glideX, glideY); + } + } + + // Reset gesture state + gestureCommitted = false; + totalDeltaX = totalDeltaY = hapticDelta = 0; + return; + } + + isGliding = false; + + // Smooth input deltas + double smoothedX = SmoothDelta(dx, bufX); + double smoothedY = SmoothDelta(dy, bufY); + + // Apply acceleration ramp + double rampedX = ApplyAccelerationRamp(smoothedX); + double rampedY = ApplyAccelerationRamp(smoothedY); + + // Accumulate gesture until committed + if (!gestureCommitted) + { + totalDeltaX += rampedX; + totalDeltaY += rampedY; + + double gestureMagSq = totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY; + if (gestureMagSq > gestureRadiusSq) + { + gestureCommitted = true; + gestureFlushX = totalDeltaX; + gestureFlushY = totalDeltaY; + + velocityHistoryX.Clear(); + velocityHistoryY.Clear(); + velocitySumX = velocitySumY = 0; + } + else return; + } + + // Track velocity using rolling sum + velocityHistoryX.Enqueue(rampedX); + velocitySumX += rampedX; + if (velocityHistoryX.Count > velocityWindowSize) + velocitySumX -= velocityHistoryX.Dequeue(); + + velocityHistoryY.Enqueue(rampedY); + velocitySumY += rampedY; + if (velocityHistoryY.Count > velocityWindowSize) + velocitySumY -= velocityHistoryY.Dequeue(); + + releaseVelocityX = velocitySumX / velocityHistoryX.Count; + releaseVelocityY = velocitySumY / velocityHistoryY.Count; + + // Apply gesture flush + double flushedX = gestureFlushX * gestureFlushRate; + double flushedY = gestureFlushY * gestureFlushRate; + gestureFlushX -= flushedX; + gestureFlushY -= flushedY; + + // Final movement vector + double finalX = flushedX + rampedX; + double finalY = flushedY + rampedY; + + // Haptic trigger + double finalMagSq = finalX * finalX + finalY * finalY; + hapticDelta += Math.Sqrt(finalMagSq); // Only one sqrt per frame + + if (hapticDelta >= hapticTriggerDelta) + { + c.Steam.SendHaptic(HapticPad.Right, HapticStyle.Weak, hapticStrength); + hapticDelta -= hapticTriggerDelta; + } + + MoveBy(finalX, finalY); + } + + // Input smoothing + private double SmoothDelta(double raw, Queue buffer) + { + buffer.Enqueue(raw); + if (buffer.Count > bufferSize) buffer.Dequeue(); + + double sum = 0; + foreach (var v in buffer) sum += v; + return sum / buffer.Count; + } + + // Acceleration ramp + private double ApplyAccelerationRamp(double delta) + { + double steepness = 0.1; + double midpoint = 6.0; + double maxBoost = 2.0; + double baseBoost = 1.7; + + double absDelta = Math.Abs(delta); + double sigmoidBoost = 1.0 + (maxBoost - 1.0) / (1.0 + Math.Exp(-steepness * (absDelta - midpoint))); + return delta * Math.Max(sigmoidBoost, baseBoost); + } + + // Glide decay + private double ApplyReleaseGlide(double velocity, double elapsed) + { + double rampedRate = 3.0 + Math.Pow(elapsed * 7.0, 2); + return velocity * Math.Exp(-elapsed * rampedRate); + } + } +} \ No newline at end of file diff --git a/SteamController/Profiles/Default/GuideShortcutsProfile.cs b/SteamController/Profiles/Default/GuideShortcutsProfile.cs index 6a88c41..48f3740 100644 --- a/SteamController/Profiles/Default/GuideShortcutsProfile.cs +++ b/SteamController/Profiles/Default/GuideShortcutsProfile.cs @@ -174,13 +174,11 @@ namespace SteamController.Profiles.Default c.Mouse[Devices.MouseController.Button.Left] = c.Steam.BtnRPadPress; } - if (c.Steam.RPadX || c.Steam.RPadY) - { - c.Mouse.MoveBy( - c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), - -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10) - ); - } + c.Mouse.MoveByFauxLizard( + c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), + -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), + c + ); } } } From 12a5ddc6bd4ca1d4356ad8e6bea90bcfa88468b5 Mon Sep 17 00:00:00 2001 From: General4878 Date: Sat, 13 Sep 2025 16:33:30 +0200 Subject: [PATCH 02/11] FLM: Send haptic on pad presses when both lizard mouse and buttons are disabled --- .../Profiles/Default/GuideShortcutsProfile.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/SteamController/Profiles/Default/GuideShortcutsProfile.cs b/SteamController/Profiles/Default/GuideShortcutsProfile.cs index 48f3740..c45106f 100644 --- a/SteamController/Profiles/Default/GuideShortcutsProfile.cs +++ b/SteamController/Profiles/Default/GuideShortcutsProfile.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using ExternalHelpers; using PowerControl.Helpers; using WindowsInput; +using static SteamController.Devices.SteamController; namespace SteamController.Profiles.Default { @@ -120,6 +121,15 @@ namespace SteamController.Profiles.Default protected void EmulateScrollOnLPad(Context c) { + //Send haptic for pad presses + if (!c.Steam.LizardButtons && !c.Steam.LizardMouse) + { + if (c.Steam.BtnLPadPress.Pressed() || c.Steam.BtnLPadPress.JustPressed()) + { + c.Steam.SendHaptic(HapticPad.Left, HapticStyle.Strong, 8); + } + } + if (c.Steam.LPadX) { c.Mouse.HorizontalScroll( @@ -174,6 +184,15 @@ namespace SteamController.Profiles.Default c.Mouse[Devices.MouseController.Button.Left] = c.Steam.BtnRPadPress; } + //Send haptic for pad presses + if (!c.Steam.LizardButtons && !c.Steam.LizardMouse) + { + if (c.Steam.BtnRPadPress.Pressed() || c.Steam.BtnRPadPress.JustPressed()) + { + c.Steam.SendHaptic(HapticPad.Right, HapticStyle.Strong, 8); + } + } + c.Mouse.MoveByFauxLizard( c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), From c536393054bde52336eeb9d4ea76bdf85cac6466 Mon Sep 17 00:00:00 2001 From: General4878 Date: Sat, 13 Sep 2025 18:05:23 +0200 Subject: [PATCH 03/11] FLM: Removed built in haptic on drag from MoveByFauxLizard into its own method and added haptic feedback for LPad drag --- .../Devices/MouseControllerFauxLizard.cs | 70 ++++++++++++++----- .../Profiles/Default/GuideShortcutsProfile.cs | 64 ++++++++++++----- 2 files changed, 100 insertions(+), 34 deletions(-) diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs index 8a6b99b..8c997a6 100644 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ b/SteamController/Devices/MouseControllerFauxLizard.cs @@ -11,15 +11,14 @@ namespace SteamController.Devices private const int velocityWindowSize = 20; // Velocity smoothing window size private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide - private const double hapticTriggerDelta = 45; // Distance threshold for haptic feedback - private const sbyte hapticStrength = 5; // Haptic feedback intensity + private const double hapticTriggerDelta = 30; // Distance threshold for haptic feedback on each pad // Functional constants (derived once) private readonly double gestureRadiusSq = gestureRadius * gestureRadius; private readonly double minGlideMagnitudeSq = minGlideMagnitude * minGlideMagnitude; private readonly double minGlideVelocitySq = minGlideVelocity * minGlideVelocity; - // Runtime state + // Runtime state RPad private Queue bufX = new(), bufY = new(); private double totalDeltaX = 0, totalDeltaY = 0; private bool gestureCommitted = false; @@ -31,13 +30,13 @@ namespace SteamController.Devices private Queue velocityHistoryX = new(), velocityHistoryY = new(); private double velocitySumX = 0, velocitySumY = 0; - private double hapticDelta = 0; + // Runtime state haptics + private double hapticDeltaR = 0; + private double hapticDeltaL = 0; // Main movement logic - public void MoveByFauxLizard(double dx, double dy, Context c) + public void MoveByFauxLizard(double dx, double dy, bool isTouched) { - bool isTouched = c.Steam.BtnRPadTouch?.LastValue ?? false; - if (!isTouched) { // Start glide if gesture was committed and velocity is high enough @@ -72,7 +71,7 @@ namespace SteamController.Devices // Reset gesture state gestureCommitted = false; - totalDeltaX = totalDeltaY = hapticDelta = 0; + totalDeltaX = totalDeltaY; return; } @@ -130,19 +129,54 @@ namespace SteamController.Devices double finalX = flushedX + rampedX; double finalY = flushedY + rampedY; - // Haptic trigger - double finalMagSq = finalX * finalX + finalY * finalY; - hapticDelta += Math.Sqrt(finalMagSq); // Only one sqrt per frame - - if (hapticDelta >= hapticTriggerDelta) - { - c.Steam.SendHaptic(HapticPad.Right, HapticStyle.Weak, hapticStrength); - hapticDelta -= hapticTriggerDelta; - } - MoveBy(finalX, finalY); } + // Checks whether the pads have been dragged enough to trigger a haptic feedback + public bool HapticDragRFauxLizard(double dx, double dy, bool isTouched) + { + if (isTouched) + { + double finalMagSq = dx * dx + dy * dy; + + if (hapticDeltaR < hapticTriggerDelta) + hapticDeltaR += Math.Sqrt(finalMagSq); + + if (hapticDeltaR >= hapticTriggerDelta) + { + hapticDeltaR -= hapticTriggerDelta; + + return true; + } + } + else + hapticDeltaR = 0; + + return false; + } + + public bool HapticDragLFauxLizard(double dx, double dy, bool isTouched) + { + if (isTouched) + { + double finalMagSq = dx * dx + dy * dy; + + if (hapticDeltaL < hapticTriggerDelta) + hapticDeltaL += Math.Sqrt(finalMagSq); + + if (hapticDeltaL >= hapticTriggerDelta) + { + hapticDeltaL -= hapticTriggerDelta; + + return true; + } + } + else + hapticDeltaL = 0; + + return false; + } + // Input smoothing private double SmoothDelta(double raw, Queue buffer) { diff --git a/SteamController/Profiles/Default/GuideShortcutsProfile.cs b/SteamController/Profiles/Default/GuideShortcutsProfile.cs index c45106f..72fb822 100644 --- a/SteamController/Profiles/Default/GuideShortcutsProfile.cs +++ b/SteamController/Profiles/Default/GuideShortcutsProfile.cs @@ -121,15 +121,6 @@ namespace SteamController.Profiles.Default protected void EmulateScrollOnLPad(Context c) { - //Send haptic for pad presses - if (!c.Steam.LizardButtons && !c.Steam.LizardMouse) - { - if (c.Steam.BtnLPadPress.Pressed() || c.Steam.BtnLPadPress.JustPressed()) - { - c.Steam.SendHaptic(HapticPad.Left, HapticStyle.Strong, 8); - } - } - if (c.Steam.LPadX) { c.Mouse.HorizontalScroll( @@ -150,6 +141,31 @@ namespace SteamController.Profiles.Default ) ); } + + if (!c.Steam.LizardButtons && !c.Steam.LizardMouse) + { + // Send haptic for pad presses + if (c.Steam.BtnLPadPress.Pressed() || c.Steam.BtnLPadPress.JustPressed()) + { + c.Steam.SendHaptic(HapticPad.Left, HapticStyle.Strong, 8); + } + + // Send haptic for pad drag + if (c.Mouse.HapticDragLFauxLizard( + c.Steam.LPadX.GetDeltaValue( + 150, + Devices.DeltaValueMode.Delta, + 10 + ), + c.Steam.LPadY.GetDeltaValue( + 150, + Devices.DeltaValueMode.Delta, + 10 + ), + c.Steam.BtnLPadTouch?.LastValue ?? false + )) + c.Steam.SendHaptic(HapticPad.Left, HapticStyle.Weak, 5); + } } protected void EmulateMouseOnRStick(Context c) @@ -184,20 +200,36 @@ namespace SteamController.Profiles.Default c.Mouse[Devices.MouseController.Button.Left] = c.Steam.BtnRPadPress; } - //Send haptic for pad presses + c.Mouse.MoveByFauxLizard( + c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), + -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), + c.Steam.BtnRPadTouch?.LastValue ?? false + ); + if (!c.Steam.LizardButtons && !c.Steam.LizardMouse) { + // Send haptic for pad presses if (c.Steam.BtnRPadPress.Pressed() || c.Steam.BtnRPadPress.JustPressed()) { c.Steam.SendHaptic(HapticPad.Right, HapticStyle.Strong, 8); } - } - c.Mouse.MoveByFauxLizard( - c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), - -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), - c - ); + // Send haptic for pad drag + if (c.Mouse.HapticDragRFauxLizard( + c.Steam.RPadX.GetDeltaValue( + 150, + Devices.DeltaValueMode.Delta, + 10 + ), + c.Steam.RPadY.GetDeltaValue( + 150, + Devices.DeltaValueMode.Delta, + 10 + ), + c.Steam.BtnRPadTouch?.LastValue ?? false + )) + c.Steam.SendHaptic(HapticPad.Right, HapticStyle.Weak, 5); + } } } } From a0243c74a65ccbc5ba2b5efb2872ceafff686ed0 Mon Sep 17 00:00:00 2001 From: General4878 Date: Sat, 13 Sep 2025 19:07:33 +0200 Subject: [PATCH 04/11] FLM: Fixed bug where total deltas were not set to zero. --- SteamController/Devices/MouseControllerFauxLizard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs index 8c997a6..9b20642 100644 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ b/SteamController/Devices/MouseControllerFauxLizard.cs @@ -71,7 +71,7 @@ namespace SteamController.Devices // Reset gesture state gestureCommitted = false; - totalDeltaX = totalDeltaY; + totalDeltaX = totalDeltaY = 0; return; } From 1d15ebebde5423d43fa9fae38288678bdc8aed46 Mon Sep 17 00:00:00 2001 From: General4878 Date: Sun, 14 Sep 2025 17:01:54 +0200 Subject: [PATCH 05/11] FLM: Added trigger buffer to haptic feedback to prevent minor movements from causing feedback. --- .../Devices/MouseControllerFauxLizard.cs | 117 +++++++++++++++--- .../Profiles/Default/GuideShortcutsProfile.cs | 8 +- 2 files changed, 102 insertions(+), 23 deletions(-) diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs index 9b20642..e38fac6 100644 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ b/SteamController/Devices/MouseControllerFauxLizard.cs @@ -5,18 +5,20 @@ namespace SteamController.Devices public partial class MouseController : IDisposable { // Adjustable parameters (tweakable settings) - private const int bufferSize = 4; // Input smoothing buffer size - private const int gestureRadius = 30; // Gesture commit threshold (pixels) - private const double gestureFlushRate = 0.1; // Rate at which gesture flush decays - private const int velocityWindowSize = 20; // Velocity smoothing window size - private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide - private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide - private const double hapticTriggerDelta = 30; // Distance threshold for haptic feedback on each pad + private const int bufferSize = 4; // Input smoothing buffer size + private const int gestureRadius = 30; // Gesture commit threshold (pixels) + private const double gestureFlushRate = 0.1; // Rate at which gesture flush decays + private const int velocityWindowSize = 20; // Velocity smoothing window size + private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide + private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide + private const double hapticTriggerDelta = 6000; // Distance required for haptic feedback on each pad + private const double hapticTriggerThreshold = 600; // Only triggers haptic if moved this much, prevents triggering on minor drifts // Functional constants (derived once) private readonly double gestureRadiusSq = gestureRadius * gestureRadius; private readonly double minGlideMagnitudeSq = minGlideMagnitude * minGlideMagnitude; private readonly double minGlideVelocitySq = minGlideVelocity * minGlideVelocity; + private readonly double hapticTriggerThresholdSq = hapticTriggerThreshold * hapticTriggerThreshold; // Runtime state RPad private Queue bufX = new(), bufY = new(); @@ -31,8 +33,21 @@ namespace SteamController.Devices private double velocitySumX = 0, velocitySumY = 0; // Runtime state haptics + private int hapticDriftBufferSize = 250; + private double hapticDeltaR = 0; + double[] hapticDriftRX = new double[250]; + double[] hapticDriftRY = new double[250]; + int hapticDriftRIndex = 0; + double hapticDriftSumRX = 0; + double hapticDriftSumRY = 0; + private double hapticDeltaL = 0; + double[] hapticDriftLX = new double[250]; + double[] hapticDriftLY = new double[250]; + int hapticDriftLIndex = 0; + double hapticDriftSumLX = 0; + double hapticDriftSumLY = 0; // Main movement logic public void MoveByFauxLizard(double dx, double dy, bool isTouched) @@ -137,20 +152,52 @@ namespace SteamController.Devices { if (isTouched) { - double finalMagSq = dx * dx + dy * dy; + // Update circular buffer and drift sums + hapticDriftSumRX -= hapticDriftRX[hapticDriftRIndex]; + hapticDriftSumRY -= hapticDriftRY[hapticDriftRIndex]; - if (hapticDeltaR < hapticTriggerDelta) - hapticDeltaR += Math.Sqrt(finalMagSq); + hapticDriftRX[hapticDriftRIndex] = dx; + hapticDriftRY[hapticDriftRIndex] = dy; - if (hapticDeltaR >= hapticTriggerDelta) + hapticDriftSumRX += dx; + hapticDriftSumRY += dy; + + hapticDriftRIndex = (hapticDriftRIndex + 1) % hapticDriftBufferSize; + + // Compute drift magnitude squared + double driftMagSq = hapticDriftSumRX * hapticDriftSumRX + hapticDriftSumRY * hapticDriftSumRY; + + if (driftMagSq > hapticTriggerThresholdSq) { - hapticDeltaR -= hapticTriggerDelta; + // Compute instantaneous movement magnitude + double deltaMagSq = dx * dx + dy * dy; + if(hapticDeltaR < hapticTriggerDelta) + hapticDeltaR += Math.Sqrt(deltaMagSq); - return true; + if (hapticDeltaR >= hapticTriggerDelta) + { + // Reset drift buffer and trigger haptic + hapticDriftSumRX = hapticDriftSumRY = 0; + hapticDriftRIndex = 0; + Array.Clear(hapticDriftRX, 0, hapticDriftBufferSize); + Array.Clear(hapticDriftRY, 0, hapticDriftBufferSize); + + hapticDeltaR -= hapticTriggerDelta; + + return true; + } } } else + { + // Reset everything on release + hapticDriftSumRX = hapticDriftSumRY = 0; + hapticDriftRIndex = 0; + Array.Clear(hapticDriftRX, 0, hapticDriftBufferSize); + Array.Clear(hapticDriftRY, 0, hapticDriftBufferSize); + hapticDeltaR = 0; + } return false; } @@ -159,20 +206,52 @@ namespace SteamController.Devices { if (isTouched) { - double finalMagSq = dx * dx + dy * dy; + // Update circular buffer and drift sums + hapticDriftSumLX -= hapticDriftLX[hapticDriftLIndex]; + hapticDriftSumLY -= hapticDriftLY[hapticDriftLIndex]; - if (hapticDeltaL < hapticTriggerDelta) - hapticDeltaL += Math.Sqrt(finalMagSq); + hapticDriftLX[hapticDriftLIndex] = dx; + hapticDriftLY[hapticDriftLIndex] = dy; - if (hapticDeltaL >= hapticTriggerDelta) + hapticDriftSumLX += dx; + hapticDriftSumLY += dy; + + hapticDriftLIndex = (hapticDriftLIndex + 1) % hapticDriftBufferSize; + + // Compute drift magnitude squared + double driftMagSq = hapticDriftSumLX * hapticDriftSumLX + hapticDriftSumLY * hapticDriftSumLY; + + if (driftMagSq > hapticTriggerThresholdSq) { - hapticDeltaL -= hapticTriggerDelta; + // Compute instantaneous movement magnitude + double deltaMagSq = dx * dx + dy * dy; + if (hapticDeltaL < hapticTriggerDelta) + hapticDeltaL += Math.Sqrt(deltaMagSq); - return true; + if (hapticDeltaL >= hapticTriggerDelta) + { + // Reset drift buffer and trigger haptic + hapticDriftSumLX = hapticDriftSumLY = 0; + hapticDriftLIndex = 0; + Array.Clear(hapticDriftLX, 0, hapticDriftBufferSize); + Array.Clear(hapticDriftLY, 0, hapticDriftBufferSize); + + hapticDeltaL -= hapticTriggerDelta; + + return true; + } } } else + { + // Reset everything on release + hapticDriftSumLX = hapticDriftSumLY = 0; + hapticDriftLIndex = 0; + Array.Clear(hapticDriftLX, 0, hapticDriftBufferSize); + Array.Clear(hapticDriftLY, 0, hapticDriftBufferSize); + hapticDeltaL = 0; + } return false; } diff --git a/SteamController/Profiles/Default/GuideShortcutsProfile.cs b/SteamController/Profiles/Default/GuideShortcutsProfile.cs index 72fb822..3726411 100644 --- a/SteamController/Profiles/Default/GuideShortcutsProfile.cs +++ b/SteamController/Profiles/Default/GuideShortcutsProfile.cs @@ -153,12 +153,12 @@ namespace SteamController.Profiles.Default // Send haptic for pad drag if (c.Mouse.HapticDragLFauxLizard( c.Steam.LPadX.GetDeltaValue( - 150, + 32766, Devices.DeltaValueMode.Delta, 10 ), c.Steam.LPadY.GetDeltaValue( - 150, + 32766, Devices.DeltaValueMode.Delta, 10 ), @@ -217,12 +217,12 @@ namespace SteamController.Profiles.Default // Send haptic for pad drag if (c.Mouse.HapticDragRFauxLizard( c.Steam.RPadX.GetDeltaValue( - 150, + 32766, Devices.DeltaValueMode.Delta, 10 ), c.Steam.RPadY.GetDeltaValue( - 150, + 32766, Devices.DeltaValueMode.Delta, 10 ), From 31c07998297f3d7ca127c0b0e49974e150a9f60a Mon Sep 17 00:00:00 2001 From: General4878 Date: Sun, 28 Sep 2025 11:18:16 +0200 Subject: [PATCH 06/11] FLM: Changed haptic feedback engine for better consistency and CPU efficiency. --- .../Devices/MouseControllerFauxLizard.cs | 317 +++++++++++++----- .../Profiles/Default/GuideShortcutsProfile.cs | 8 +- 2 files changed, 237 insertions(+), 88 deletions(-) diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs index e38fac6..7da8999 100644 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ b/SteamController/Devices/MouseControllerFauxLizard.cs @@ -1,3 +1,4 @@ +using System; using static SteamController.Devices.SteamController; namespace SteamController.Devices @@ -11,14 +12,14 @@ namespace SteamController.Devices private const int velocityWindowSize = 20; // Velocity smoothing window size private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide - private const double hapticTriggerDelta = 6000; // Distance required for haptic feedback on each pad - private const double hapticTriggerThreshold = 600; // Only triggers haptic if moved this much, prevents triggering on minor drifts + private const int hapticBufferSize = 10; // Haptic smoothing and jitter reduction buffer + private const double hapticTriggerDelta = 30; // Distance required for haptic feedback on each pad // Functional constants (derived once) private readonly double gestureRadiusSq = gestureRadius * gestureRadius; private readonly double minGlideMagnitudeSq = minGlideMagnitude * minGlideMagnitude; private readonly double minGlideVelocitySq = minGlideVelocity * minGlideVelocity; - private readonly double hapticTriggerThresholdSq = hapticTriggerThreshold * hapticTriggerThreshold; + private readonly double hapticBufferMidpoint = hapticBufferSize / 2.0; // Runtime state RPad private Queue bufX = new(), bufY = new(); @@ -33,21 +34,39 @@ namespace SteamController.Devices private double velocitySumX = 0, velocitySumY = 0; // Runtime state haptics - private int hapticDriftBufferSize = 250; + private readonly int[] hapticDxSignBufferR = new int[hapticBufferSize]; + private readonly int[] hapticDySignBufferR = new int[hapticBufferSize]; + private readonly int[] hapticDxFlipFlagBufferR = new int[hapticBufferSize]; + private readonly int[] hapticDyFlipFlagBufferR = new int[hapticBufferSize]; + private readonly double[] hapticMagBufferR = new double[hapticBufferSize]; private double hapticDeltaR = 0; - double[] hapticDriftRX = new double[250]; - double[] hapticDriftRY = new double[250]; - int hapticDriftRIndex = 0; - double hapticDriftSumRX = 0; - double hapticDriftSumRY = 0; + private int hapticBufferCountR = 0; + private int hapticBufferIndexR = 0; + private int hapticDxFlipCountR = 0; + private int hapticDyFlipCountR = 0; + private int hapticDxZeroCountR = 0; + private int hapticDyZeroCountR = 0; + private double hapticMagSumR = 0; + + private bool hapticClearedR = true; + + private readonly int[] hapticDxSignBufferL = new int[hapticBufferSize]; + private readonly int[] hapticDySignBufferL = new int[hapticBufferSize]; + private readonly int[] hapticDxFlipFlagBufferL = new int[hapticBufferSize]; + private readonly int[] hapticDyFlipFlagBufferL = new int[hapticBufferSize]; + private readonly double[] hapticMagBufferL = new double[hapticBufferSize]; private double hapticDeltaL = 0; - double[] hapticDriftLX = new double[250]; - double[] hapticDriftLY = new double[250]; - int hapticDriftLIndex = 0; - double hapticDriftSumLX = 0; - double hapticDriftSumLY = 0; + private int hapticBufferCountL = 0; + private int hapticBufferIndexL = 0; + private int hapticDxFlipCountL = 0; + private int hapticDyFlipCountL = 0; + private int hapticDxZeroCountL = 0; + private int hapticDyZeroCountL = 0; + private double hapticMagSumL = 0; + + private bool hapticClearedL = true; // Main movement logic public void MoveByFauxLizard(double dx, double dy, bool isTouched) @@ -152,51 +171,116 @@ namespace SteamController.Devices { if (isTouched) { - // Update circular buffer and drift sums - hapticDriftSumRX -= hapticDriftRX[hapticDriftRIndex]; - hapticDriftSumRY -= hapticDriftRY[hapticDriftRIndex]; + hapticClearedR = false; - hapticDriftRX[hapticDriftRIndex] = dx; - hapticDriftRY[hapticDriftRIndex] = dy; - - hapticDriftSumRX += dx; - hapticDriftSumRY += dy; - - hapticDriftRIndex = (hapticDriftRIndex + 1) % hapticDriftBufferSize; - - // Compute drift magnitude squared - double driftMagSq = hapticDriftSumRX * hapticDriftSumRX + hapticDriftSumRY * hapticDriftSumRY; - - if (driftMagSq > hapticTriggerThresholdSq) + // If buffer is full, remove array contributions at this slot + if (hapticBufferCountR == hapticBufferSize) { - // Compute instantaneous movement magnitude - double deltaMagSq = dx * dx + dy * dy; - if(hapticDeltaR < hapticTriggerDelta) - hapticDeltaR += Math.Sqrt(deltaMagSq); + int idx = hapticBufferIndexR; + hapticDxFlipCountR -= hapticDxFlipFlagBufferR[idx]; + hapticDyFlipCountR -= hapticDyFlipFlagBufferR[idx]; + if (hapticDxSignBufferR[idx] == 0) hapticDxZeroCountR--; + if (hapticDySignBufferR[idx] == 0) hapticDyZeroCountR--; + hapticMagSumR -= hapticMagBufferR[idx]; + } + else + { + hapticBufferCountR++; + } - if (hapticDeltaR >= hapticTriggerDelta) - { - // Reset drift buffer and trigger haptic - hapticDriftSumRX = hapticDriftSumRY = 0; - hapticDriftRIndex = 0; - Array.Clear(hapticDriftRX, 0, hapticDriftBufferSize); - Array.Clear(hapticDriftRY, 0, hapticDriftBufferSize); + // Compute current signs + int curDxSign = Math.Sign(dx); + int curDySign = Math.Sign(dy); - hapticDeltaR -= hapticTriggerDelta; + // Compute flips against previous signs + int dxFlip = 0, dyFlip = 0; + if (hapticBufferCountR > 1) + { + int prevIndex = (hapticBufferIndexR - 1 + hapticBufferSize) % hapticBufferSize; + int prevDxSign = hapticDxSignBufferR[prevIndex]; + int prevDySign = hapticDySignBufferR[prevIndex]; - return true; - } + if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; + if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; + } + + // Compute current magnitude + double mag = Math.Sqrt(dx * dx + dy * dy); + + // Store signs and flip flags and magnitude in array for rolling sums + int i = hapticBufferIndexR; + hapticDxSignBufferR[i] = curDxSign; + hapticDySignBufferR[i] = curDySign; + hapticDxFlipFlagBufferR[i] = dxFlip; + hapticDyFlipFlagBufferR[i] = dyFlip; + hapticMagBufferR[i] = mag; + + // Increment rolling sums + if (curDxSign == 0) hapticDxZeroCountR++; + if (curDySign == 0) hapticDyZeroCountR++; + hapticDxFlipCountR += dxFlip; + hapticDyFlipCountR += dyFlip; + hapticMagSumR += mag; + + // Store new buffer index for later + hapticBufferIndexR++; + if (hapticBufferIndexR == hapticBufferSize) hapticBufferIndexR = 0; + + // No need to proceed as buffer is not full yet + if (hapticBufferCountR != hapticBufferSize) + return false; + + // Compute average magnitude over buffer for smoothing + double avgMag = hapticMagSumR / hapticBufferCountR; + + // Calculate a penalty for the magnitude + double bufferSpan = hapticBufferSize - hapticBufferMidpoint; + double invSpan = 1.0 / bufferSpan; + bool dxZeroEdge = hapticDxZeroCountR == hapticBufferCountR; + bool dyZeroEdge = hapticDyZeroCountR == hapticBufferCountR; + double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; + + double dxFactor = dxZeroEdge + ? 0.0 + : (hapticDxFlipCountR < hapticBufferMidpoint + ? factor + : -factor * ((hapticDxFlipCountR - hapticBufferMidpoint) * invSpan)); + + double dyFactor = dyZeroEdge + ? 0.0 + : (hapticDyFlipCountR < hapticBufferMidpoint + ? factor + : -factor * ((hapticDyFlipCountR - hapticBufferMidpoint) * invSpan)); + + // Final penalty + hapticDeltaR += avgMag * (dxFactor + dyFactor); + + if (hapticDeltaR >= hapticTriggerDelta) + { + hapticDeltaR -= hapticTriggerDelta; + return true; + } + else if (hapticDeltaR < 0) + { + hapticDeltaR = 0; } } else { - // Reset everything on release - hapticDriftSumRX = hapticDriftSumRY = 0; - hapticDriftRIndex = 0; - Array.Clear(hapticDriftRX, 0, hapticDriftBufferSize); - Array.Clear(hapticDriftRY, 0, hapticDriftBufferSize); + if (!hapticClearedR) + { + hapticClearedR = true; - hapticDeltaR = 0; + // Reset all buffers when not touched + hapticDeltaR = 0; + hapticBufferCountR = 0; + hapticBufferIndexR = 0; + hapticDxFlipCountR = 0; + hapticDyFlipCountR = 0; + hapticDxZeroCountR = 0; + hapticDyZeroCountR = 0; + hapticMagSumR = 0; + } } return false; @@ -206,51 +290,116 @@ namespace SteamController.Devices { if (isTouched) { - // Update circular buffer and drift sums - hapticDriftSumLX -= hapticDriftLX[hapticDriftLIndex]; - hapticDriftSumLY -= hapticDriftLY[hapticDriftLIndex]; + hapticClearedL = false; - hapticDriftLX[hapticDriftLIndex] = dx; - hapticDriftLY[hapticDriftLIndex] = dy; - - hapticDriftSumLX += dx; - hapticDriftSumLY += dy; - - hapticDriftLIndex = (hapticDriftLIndex + 1) % hapticDriftBufferSize; - - // Compute drift magnitude squared - double driftMagSq = hapticDriftSumLX * hapticDriftSumLX + hapticDriftSumLY * hapticDriftSumLY; - - if (driftMagSq > hapticTriggerThresholdSq) + // If buffer is full, remove array contributions at this slot + if (hapticBufferCountL == hapticBufferSize) { - // Compute instantaneous movement magnitude - double deltaMagSq = dx * dx + dy * dy; - if (hapticDeltaL < hapticTriggerDelta) - hapticDeltaL += Math.Sqrt(deltaMagSq); + int idx = hapticBufferIndexL; + hapticDxFlipCountL -= hapticDxFlipFlagBufferL[idx]; + hapticDyFlipCountL -= hapticDyFlipFlagBufferL[idx]; + if (hapticDxSignBufferL[idx] == 0) hapticDxZeroCountL--; + if (hapticDySignBufferL[idx] == 0) hapticDyZeroCountL--; + hapticMagSumL -= hapticMagBufferL[idx]; + } + else + { + hapticBufferCountL++; + } - if (hapticDeltaL >= hapticTriggerDelta) - { - // Reset drift buffer and trigger haptic - hapticDriftSumLX = hapticDriftSumLY = 0; - hapticDriftLIndex = 0; - Array.Clear(hapticDriftLX, 0, hapticDriftBufferSize); - Array.Clear(hapticDriftLY, 0, hapticDriftBufferSize); + // Compute current signs + int curDxSign = Math.Sign(dx); + int curDySign = Math.Sign(dy); - hapticDeltaL -= hapticTriggerDelta; + // Compute flips against previous signs + int dxFlip = 0, dyFlip = 0; + if (hapticBufferCountL > 1) + { + int prevIndex = (hapticBufferIndexL - 1 + hapticBufferSize) % hapticBufferSize; + int prevDxSign = hapticDxSignBufferL[prevIndex]; + int prevDySign = hapticDySignBufferL[prevIndex]; - return true; - } + if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; + if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; + } + + // Compute current magnitude + double mag = Math.Sqrt(dx * dx + dy * dy); + + // Store signs and flip flags and magnitude in array for rolling sums + int i = hapticBufferIndexL; + hapticDxSignBufferL[i] = curDxSign; + hapticDySignBufferL[i] = curDySign; + hapticDxFlipFlagBufferL[i] = dxFlip; + hapticDyFlipFlagBufferL[i] = dyFlip; + hapticMagBufferL[i] = mag; + + // Increment rolling sums + if (curDxSign == 0) hapticDxZeroCountL++; + if (curDySign == 0) hapticDyZeroCountL++; + hapticDxFlipCountL += dxFlip; + hapticDyFlipCountL += dyFlip; + hapticMagSumL += mag; + + // Store new buffer index for later + hapticBufferIndexL++; + if (hapticBufferIndexL == hapticBufferSize) hapticBufferIndexL = 0; + + // No need to proceed as buffer is not full yet + if (hapticBufferCountL != hapticBufferSize) + return false; + + // Compute average magnitude over buffer for smoothing + double avgMag = hapticMagSumL / hapticBufferCountL; + + // Calculate a penalty for the magnitude + double bufferSpan = hapticBufferSize - hapticBufferMidpoint; + double invSpan = 1.0 / bufferSpan; + bool dxZeroEdge = hapticDxZeroCountL == hapticBufferCountL; + bool dyZeroEdge = hapticDyZeroCountL == hapticBufferCountL; + double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; + + double dxFactor = dxZeroEdge + ? 0.0 + : (hapticDxFlipCountL < hapticBufferMidpoint + ? factor + : -factor * ((hapticDxFlipCountL - hapticBufferMidpoint) * invSpan)); + + double dyFactor = dyZeroEdge + ? 0.0 + : (hapticDyFlipCountL < hapticBufferMidpoint + ? factor + : -factor * ((hapticDyFlipCountL - hapticBufferMidpoint) * invSpan)); + + // Final penalty + hapticDeltaL += avgMag * (dxFactor + dyFactor); + + if (hapticDeltaL >= hapticTriggerDelta) + { + hapticDeltaL -= hapticTriggerDelta; + return true; + } + else if (hapticDeltaL < 0) + { + hapticDeltaL = 0; } } else { - // Reset everything on release - hapticDriftSumLX = hapticDriftSumLY = 0; - hapticDriftLIndex = 0; - Array.Clear(hapticDriftLX, 0, hapticDriftBufferSize); - Array.Clear(hapticDriftLY, 0, hapticDriftBufferSize); + if (!hapticClearedL) + { + hapticClearedL = true; - hapticDeltaL = 0; + // Reset all buffers when not touched + hapticDeltaL = 0; + hapticBufferCountL = 0; + hapticBufferIndexL = 0; + hapticDxFlipCountL = 0; + hapticDyFlipCountL = 0; + hapticDxZeroCountL = 0; + hapticDyZeroCountL = 0; + hapticMagSumL = 0; + } } return false; diff --git a/SteamController/Profiles/Default/GuideShortcutsProfile.cs b/SteamController/Profiles/Default/GuideShortcutsProfile.cs index 3726411..72fb822 100644 --- a/SteamController/Profiles/Default/GuideShortcutsProfile.cs +++ b/SteamController/Profiles/Default/GuideShortcutsProfile.cs @@ -153,12 +153,12 @@ namespace SteamController.Profiles.Default // Send haptic for pad drag if (c.Mouse.HapticDragLFauxLizard( c.Steam.LPadX.GetDeltaValue( - 32766, + 150, Devices.DeltaValueMode.Delta, 10 ), c.Steam.LPadY.GetDeltaValue( - 32766, + 150, Devices.DeltaValueMode.Delta, 10 ), @@ -217,12 +217,12 @@ namespace SteamController.Profiles.Default // Send haptic for pad drag if (c.Mouse.HapticDragRFauxLizard( c.Steam.RPadX.GetDeltaValue( - 32766, + 150, Devices.DeltaValueMode.Delta, 10 ), c.Steam.RPadY.GetDeltaValue( - 32766, + 150, Devices.DeltaValueMode.Delta, 10 ), From d7db9bea8d902c71c113088c2c0c5d5db2bec5d0 Mon Sep 17 00:00:00 2001 From: General4878 Date: Mon, 29 Sep 2025 22:02:19 +0200 Subject: [PATCH 07/11] FLM: Making sure that hapticDeltaR is always below hapticTriggerDelta when haptic is triggered to prevent multiple triggers. Added a temporal decay mechanism for haptics to prevent triggering by jitter. --- .../Devices/MouseControllerFauxLizard.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs index 7da8999..a087afd 100644 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ b/SteamController/Devices/MouseControllerFauxLizard.cs @@ -12,8 +12,9 @@ namespace SteamController.Devices private const int velocityWindowSize = 20; // Velocity smoothing window size private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide - private const int hapticBufferSize = 10; // Haptic smoothing and jitter reduction buffer + private const int hapticBufferSize = 10; // Haptic smoothing and jitter reduction buffer private const double hapticTriggerDelta = 30; // Distance required for haptic feedback on each pad + private const int hapticResetTime = 5; // Number of seconds to invalidate a haptic feedback, prevents triggering on drag jitter // Functional constants (derived once) private readonly double gestureRadiusSq = gestureRadius * gestureRadius; @@ -34,6 +35,7 @@ namespace SteamController.Devices private double velocitySumX = 0, velocitySumY = 0; // Runtime state haptics + // Right track pad private readonly int[] hapticDxSignBufferR = new int[hapticBufferSize]; private readonly int[] hapticDySignBufferR = new int[hapticBufferSize]; private readonly int[] hapticDxFlipFlagBufferR = new int[hapticBufferSize]; @@ -48,9 +50,11 @@ namespace SteamController.Devices private int hapticDxZeroCountR = 0; private int hapticDyZeroCountR = 0; private double hapticMagSumR = 0; + private DateTime hapticLastTimeR = DateTime.MinValue; private bool hapticClearedR = true; + // Left track pad private readonly int[] hapticDxSignBufferL = new int[hapticBufferSize]; private readonly int[] hapticDySignBufferL = new int[hapticBufferSize]; private readonly int[] hapticDxFlipFlagBufferL = new int[hapticBufferSize]; @@ -65,6 +69,7 @@ namespace SteamController.Devices private int hapticDxZeroCountL = 0; private int hapticDyZeroCountL = 0; private double hapticMagSumL = 0; + private DateTime hapticLastTimeL = DateTime.MinValue; private bool hapticClearedL = true; @@ -173,6 +178,18 @@ namespace SteamController.Devices { hapticClearedR = false; + // Timeout check: reset delta if accumulation is too slow + DateTime now = DateTime.UtcNow; + if (hapticBufferCountR == 0) + { + hapticLastTimeR = now; + } + else if ((now - hapticLastTimeR).TotalSeconds > hapticResetTime) + { + hapticDeltaR = 0; + hapticLastTimeR = now; + } + // If buffer is full, remove array contributions at this slot if (hapticBufferCountR == hapticBufferSize) { @@ -257,7 +274,8 @@ namespace SteamController.Devices if (hapticDeltaR >= hapticTriggerDelta) { - hapticDeltaR -= hapticTriggerDelta; + hapticDeltaR -= Math.Floor(hapticDeltaR / hapticTriggerDelta) * hapticTriggerDelta; + hapticLastTimeR = now; return true; } else if (hapticDeltaR < 0) @@ -292,6 +310,18 @@ namespace SteamController.Devices { hapticClearedL = false; + // Timeout check: reset delta if accumulation is too slow + DateTime now = DateTime.UtcNow; + if (hapticBufferCountL == 0) + { + hapticLastTimeL = now; + } + else if ((now - hapticLastTimeL).TotalSeconds > hapticResetTime) + { + hapticDeltaL = 0; + hapticLastTimeL = now; + } + // If buffer is full, remove array contributions at this slot if (hapticBufferCountL == hapticBufferSize) { @@ -376,7 +406,8 @@ namespace SteamController.Devices if (hapticDeltaL >= hapticTriggerDelta) { - hapticDeltaL -= hapticTriggerDelta; + hapticDeltaL -= Math.Floor(hapticDeltaL / hapticTriggerDelta) * hapticTriggerDelta; + hapticLastTimeL = now; return true; } else if (hapticDeltaL < 0) From d232aef11d98cc798a85358e1eeed5e15042e552 Mon Sep 17 00:00:00 2001 From: General4878 Date: Sun, 5 Oct 2025 16:20:04 +0200 Subject: [PATCH 08/11] FLM: Added debug setting to enable/disable faux lizard mode. --- .../Profiles/Default/GuideShortcutsProfile.cs | 27 ++++++++++++++----- SteamController/SettingsDebug.cs | 7 +++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/SteamController/Profiles/Default/GuideShortcutsProfile.cs b/SteamController/Profiles/Default/GuideShortcutsProfile.cs index 72fb822..be29c27 100644 --- a/SteamController/Profiles/Default/GuideShortcutsProfile.cs +++ b/SteamController/Profiles/Default/GuideShortcutsProfile.cs @@ -142,7 +142,7 @@ namespace SteamController.Profiles.Default ); } - if (!c.Steam.LizardButtons && !c.Steam.LizardMouse) + if (SettingsDebug.Default.FauxLizardMode && !c.Steam.LizardButtons && !c.Steam.LizardMouse) { // Send haptic for pad presses if (c.Steam.BtnLPadPress.Pressed() || c.Steam.BtnLPadPress.JustPressed()) @@ -200,14 +200,16 @@ namespace SteamController.Profiles.Default c.Mouse[Devices.MouseController.Button.Left] = c.Steam.BtnRPadPress; } - c.Mouse.MoveByFauxLizard( - c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), - -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), - c.Steam.BtnRPadTouch?.LastValue ?? false - ); + bool simpleEmulation = true; - if (!c.Steam.LizardButtons && !c.Steam.LizardMouse) + if (SettingsDebug.Default.FauxLizardMode && !c.Steam.LizardButtons && !c.Steam.LizardMouse) { + c.Mouse.MoveByFauxLizard( + c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), + -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), + c.Steam.BtnRPadTouch?.LastValue ?? false + ); + // Send haptic for pad presses if (c.Steam.BtnRPadPress.Pressed() || c.Steam.BtnRPadPress.JustPressed()) { @@ -229,6 +231,17 @@ namespace SteamController.Profiles.Default c.Steam.BtnRPadTouch?.LastValue ?? false )) c.Steam.SendHaptic(HapticPad.Right, HapticStyle.Weak, 5); + + // We do not want simple emulation after faux lizard emulation + simpleEmulation = false; + } + + if (simpleEmulation && (c.Steam.RPadX || c.Steam.RPadY)) + { + c.Mouse.MoveBy( + c.Steam.RPadX.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10), + -c.Steam.RPadY.GetDeltaValue(Context.PadToMouseSensitivity, Devices.DeltaValueMode.Delta, 10) + ); } } } diff --git a/SteamController/SettingsDebug.cs b/SteamController/SettingsDebug.cs index 54242d3..cd778f8 100644 --- a/SteamController/SettingsDebug.cs +++ b/SteamController/SettingsDebug.cs @@ -27,6 +27,13 @@ namespace SteamController set { Set("LizardMouse", value); } } + [Description("Emulate Lizard controls in software. LizardButtons and LizardMouse must be disabled for this to take effect.")] + public bool FauxLizardMode + { + get { return Get("FauxLizardMode", false); } + set { Set("FauxLizardMode", value); } + } + public override string ToString() { return ""; From 0ab15fb6ac6faf8fc3427840f605759c2a01d7a8 Mon Sep 17 00:00:00 2001 From: General4878 Date: Sun, 28 Dec 2025 14:31:55 +0100 Subject: [PATCH 09/11] FLM: Performance imporvements: replaced queues with arrays and rolling sums, and replaced datetime with stopwatch. --- .../Devices/MouseControllerFauxLizard.cs | 96 +++++++++++++------ 1 file changed, 66 insertions(+), 30 deletions(-) diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs index a087afd..0f6d854 100644 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ b/SteamController/Devices/MouseControllerFauxLizard.cs @@ -23,16 +23,25 @@ namespace SteamController.Devices private readonly double hapticBufferMidpoint = hapticBufferSize / 2.0; // Runtime state RPad - private Queue bufX = new(), bufY = new(); + private readonly double[] bufX = new double[bufferSize]; + private int bufXIndex = 0, bufXCount = 0; + private double bufXSum = 0; + private readonly double[] bufY = new double[bufferSize]; + private int bufYIndex = 0, bufYCount = 0; + private double bufYSum = 0; private double totalDeltaX = 0, totalDeltaY = 0; private bool gestureCommitted = false; private double gestureFlushX = 0, gestureFlushY = 0; private bool isGliding = false; - private DateTime releaseTime; + private long releaseTicks; private double releaseVelocityX = 0, releaseVelocityY = 0; - private Queue velocityHistoryX = new(), velocityHistoryY = new(); - private double velocitySumX = 0, velocitySumY = 0; + private readonly double[] velocityX = new double[velocityWindowSize]; + private int velocityXIndex = 0, velocityXCount = 0; + private double velocityXSum = 0; + private readonly double[] velocityY = new double[velocityWindowSize]; + private int velocityYIndex = 0, velocityYCount = 0; + private double velocityYSum = 0; // Runtime state haptics // Right track pad @@ -84,7 +93,7 @@ namespace SteamController.Devices double magSq = releaseVelocityX * releaseVelocityX + releaseVelocityY * releaseVelocityY; if (magSq >= minGlideMagnitudeSq) { - releaseTime = DateTime.Now; + releaseTicks = System.Diagnostics.Stopwatch.GetTimestamp(); isGliding = true; } } @@ -92,7 +101,9 @@ namespace SteamController.Devices // Continue glide if active if (isGliding) { - double elapsed = (DateTime.Now - releaseTime).TotalSeconds; + double elapsed = + (System.Diagnostics.Stopwatch.GetTimestamp() - releaseTicks) + / (double)System.Diagnostics.Stopwatch.Frequency; double glideX = ApplyReleaseGlide(releaseVelocityX, elapsed); double glideY = ApplyReleaseGlide(releaseVelocityY, elapsed); @@ -110,15 +121,16 @@ namespace SteamController.Devices // Reset gesture state gestureCommitted = false; - totalDeltaX = totalDeltaY = 0; + bufXIndex = bufXCount = bufYIndex = bufYCount = velocityXIndex = velocityXCount = velocityYIndex = velocityYCount = 0; + bufYSum = bufXSum = velocityXSum = velocityYSum = totalDeltaX = totalDeltaY = 0; return; } isGliding = false; // Smooth input deltas - double smoothedX = SmoothDelta(dx, bufX); - double smoothedY = SmoothDelta(dy, bufY); + double smoothedX = SmoothDelta(dx, bufX, ref bufXIndex, ref bufXCount, ref bufXSum); + double smoothedY = SmoothDelta(dy, bufY, ref bufYIndex, ref bufYCount, ref bufYSum); // Apply acceleration ramp double rampedX = ApplyAccelerationRamp(smoothedX); @@ -137,26 +149,15 @@ namespace SteamController.Devices gestureFlushX = totalDeltaX; gestureFlushY = totalDeltaY; - velocityHistoryX.Clear(); - velocityHistoryY.Clear(); - velocitySumX = velocitySumY = 0; + velocityXIndex = velocityXCount = 0; velocityXSum = 0; + velocityYIndex = velocityYCount = 0; velocityYSum = 0; } else return; } // Track velocity using rolling sum - velocityHistoryX.Enqueue(rampedX); - velocitySumX += rampedX; - if (velocityHistoryX.Count > velocityWindowSize) - velocitySumX -= velocityHistoryX.Dequeue(); - - velocityHistoryY.Enqueue(rampedY); - velocitySumY += rampedY; - if (velocityHistoryY.Count > velocityWindowSize) - velocitySumY -= velocityHistoryY.Dequeue(); - - releaseVelocityX = velocitySumX / velocityHistoryX.Count; - releaseVelocityY = velocitySumY / velocityHistoryY.Count; + releaseVelocityX = SmoothVelocity(rampedX, velocityX, ref velocityXIndex, ref velocityXCount, ref velocityXSum); + releaseVelocityY = SmoothVelocity(rampedY, velocityY, ref velocityYIndex, ref velocityYCount, ref velocityYSum); // Apply gesture flush double flushedX = gestureFlushX * gestureFlushRate; @@ -437,14 +438,49 @@ namespace SteamController.Devices } // Input smoothing - private double SmoothDelta(double raw, Queue buffer) + private double SmoothDelta(double raw, double[] buffer, ref int index, ref int count, ref double sum) { - buffer.Enqueue(raw); - if (buffer.Count > bufferSize) buffer.Dequeue(); + if (count < bufferSize) + { + buffer[index] = raw; + sum += raw; + count++; + } + else + { + sum -= buffer[index]; + buffer[index] = raw; + sum += raw; + } - double sum = 0; - foreach (var v in buffer) sum += v; - return sum / buffer.Count; + index++; + if (index == bufferSize) + index = 0; + + return sum / count; + } + + // Velocity smoothing + private double SmoothVelocity(double v, double[] buffer, ref int index, ref int count, ref double sum) + { + if (count < velocityWindowSize) + { + buffer[index] = v; + sum += v; + count++; + } + else + { + sum -= buffer[index]; + buffer[index] = v; + sum += v; + } + + index++; + if (index == velocityWindowSize) + index = 0; + + return sum / count; } // Acceleration ramp From abbe6094f3b6482a05af2a6e1b74db0861b22b6c Mon Sep 17 00:00:00 2001 From: General4878 Date: Sun, 28 Dec 2025 16:43:34 +0100 Subject: [PATCH 10/11] FLM: Code tidy up to match ayufan's style, and reduce repetition. --- .../Devices/MouseControllerFauxLizard.cs | 506 ------------------ .../MouseControllerFauxLizardHaptic.cs | 199 +++++++ .../Devices/MouseControllerFauxLizardMove.cs | 198 +++++++ 3 files changed, 397 insertions(+), 506 deletions(-) delete mode 100644 SteamController/Devices/MouseControllerFauxLizard.cs create mode 100644 SteamController/Devices/MouseControllerFauxLizardHaptic.cs create mode 100644 SteamController/Devices/MouseControllerFauxLizardMove.cs diff --git a/SteamController/Devices/MouseControllerFauxLizard.cs b/SteamController/Devices/MouseControllerFauxLizard.cs deleted file mode 100644 index 0f6d854..0000000 --- a/SteamController/Devices/MouseControllerFauxLizard.cs +++ /dev/null @@ -1,506 +0,0 @@ -using System; -using static SteamController.Devices.SteamController; - -namespace SteamController.Devices -{ - public partial class MouseController : IDisposable - { - // Adjustable parameters (tweakable settings) - private const int bufferSize = 4; // Input smoothing buffer size - private const int gestureRadius = 30; // Gesture commit threshold (pixels) - private const double gestureFlushRate = 0.1; // Rate at which gesture flush decays - private const int velocityWindowSize = 20; // Velocity smoothing window size - private const double minGlideMagnitude = 0.8; // Minimum velocity to start glide - private const double minGlideVelocity = 0.15; // Minimum velocity to continue glide - private const int hapticBufferSize = 10; // Haptic smoothing and jitter reduction buffer - private const double hapticTriggerDelta = 30; // Distance required for haptic feedback on each pad - private const int hapticResetTime = 5; // Number of seconds to invalidate a haptic feedback, prevents triggering on drag jitter - - // Functional constants (derived once) - private readonly double gestureRadiusSq = gestureRadius * gestureRadius; - private readonly double minGlideMagnitudeSq = minGlideMagnitude * minGlideMagnitude; - private readonly double minGlideVelocitySq = minGlideVelocity * minGlideVelocity; - private readonly double hapticBufferMidpoint = hapticBufferSize / 2.0; - - // Runtime state RPad - private readonly double[] bufX = new double[bufferSize]; - private int bufXIndex = 0, bufXCount = 0; - private double bufXSum = 0; - private readonly double[] bufY = new double[bufferSize]; - private int bufYIndex = 0, bufYCount = 0; - private double bufYSum = 0; - private double totalDeltaX = 0, totalDeltaY = 0; - private bool gestureCommitted = false; - private double gestureFlushX = 0, gestureFlushY = 0; - - private bool isGliding = false; - private long releaseTicks; - private double releaseVelocityX = 0, releaseVelocityY = 0; - private readonly double[] velocityX = new double[velocityWindowSize]; - private int velocityXIndex = 0, velocityXCount = 0; - private double velocityXSum = 0; - private readonly double[] velocityY = new double[velocityWindowSize]; - private int velocityYIndex = 0, velocityYCount = 0; - private double velocityYSum = 0; - - // Runtime state haptics - // Right track pad - private readonly int[] hapticDxSignBufferR = new int[hapticBufferSize]; - private readonly int[] hapticDySignBufferR = new int[hapticBufferSize]; - private readonly int[] hapticDxFlipFlagBufferR = new int[hapticBufferSize]; - private readonly int[] hapticDyFlipFlagBufferR = new int[hapticBufferSize]; - private readonly double[] hapticMagBufferR = new double[hapticBufferSize]; - - private double hapticDeltaR = 0; - private int hapticBufferCountR = 0; - private int hapticBufferIndexR = 0; - private int hapticDxFlipCountR = 0; - private int hapticDyFlipCountR = 0; - private int hapticDxZeroCountR = 0; - private int hapticDyZeroCountR = 0; - private double hapticMagSumR = 0; - private DateTime hapticLastTimeR = DateTime.MinValue; - - private bool hapticClearedR = true; - - // Left track pad - private readonly int[] hapticDxSignBufferL = new int[hapticBufferSize]; - private readonly int[] hapticDySignBufferL = new int[hapticBufferSize]; - private readonly int[] hapticDxFlipFlagBufferL = new int[hapticBufferSize]; - private readonly int[] hapticDyFlipFlagBufferL = new int[hapticBufferSize]; - private readonly double[] hapticMagBufferL = new double[hapticBufferSize]; - - private double hapticDeltaL = 0; - private int hapticBufferCountL = 0; - private int hapticBufferIndexL = 0; - private int hapticDxFlipCountL = 0; - private int hapticDyFlipCountL = 0; - private int hapticDxZeroCountL = 0; - private int hapticDyZeroCountL = 0; - private double hapticMagSumL = 0; - private DateTime hapticLastTimeL = DateTime.MinValue; - - private bool hapticClearedL = true; - - // Main movement logic - public void MoveByFauxLizard(double dx, double dy, bool isTouched) - { - if (!isTouched) - { - // Start glide if gesture was committed and velocity is high enough - if (!isGliding && gestureCommitted) - { - double magSq = releaseVelocityX * releaseVelocityX + releaseVelocityY * releaseVelocityY; - if (magSq >= minGlideMagnitudeSq) - { - releaseTicks = System.Diagnostics.Stopwatch.GetTimestamp(); - isGliding = true; - } - } - - // Continue glide if active - if (isGliding) - { - double elapsed = - (System.Diagnostics.Stopwatch.GetTimestamp() - releaseTicks) - / (double)System.Diagnostics.Stopwatch.Frequency; - double glideX = ApplyReleaseGlide(releaseVelocityX, elapsed); - double glideY = ApplyReleaseGlide(releaseVelocityY, elapsed); - - double glideMagSq = glideX * glideX + glideY * glideY; - if (glideMagSq < minGlideVelocitySq) - { - isGliding = false; - releaseVelocityX = releaseVelocityY = 0; - } - else - { - MoveBy(glideX, glideY); - } - } - - // Reset gesture state - gestureCommitted = false; - bufXIndex = bufXCount = bufYIndex = bufYCount = velocityXIndex = velocityXCount = velocityYIndex = velocityYCount = 0; - bufYSum = bufXSum = velocityXSum = velocityYSum = totalDeltaX = totalDeltaY = 0; - return; - } - - isGliding = false; - - // Smooth input deltas - double smoothedX = SmoothDelta(dx, bufX, ref bufXIndex, ref bufXCount, ref bufXSum); - double smoothedY = SmoothDelta(dy, bufY, ref bufYIndex, ref bufYCount, ref bufYSum); - - // Apply acceleration ramp - double rampedX = ApplyAccelerationRamp(smoothedX); - double rampedY = ApplyAccelerationRamp(smoothedY); - - // Accumulate gesture until committed - if (!gestureCommitted) - { - totalDeltaX += rampedX; - totalDeltaY += rampedY; - - double gestureMagSq = totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY; - if (gestureMagSq > gestureRadiusSq) - { - gestureCommitted = true; - gestureFlushX = totalDeltaX; - gestureFlushY = totalDeltaY; - - velocityXIndex = velocityXCount = 0; velocityXSum = 0; - velocityYIndex = velocityYCount = 0; velocityYSum = 0; - } - else return; - } - - // Track velocity using rolling sum - releaseVelocityX = SmoothVelocity(rampedX, velocityX, ref velocityXIndex, ref velocityXCount, ref velocityXSum); - releaseVelocityY = SmoothVelocity(rampedY, velocityY, ref velocityYIndex, ref velocityYCount, ref velocityYSum); - - // Apply gesture flush - double flushedX = gestureFlushX * gestureFlushRate; - double flushedY = gestureFlushY * gestureFlushRate; - gestureFlushX -= flushedX; - gestureFlushY -= flushedY; - - // Final movement vector - double finalX = flushedX + rampedX; - double finalY = flushedY + rampedY; - - MoveBy(finalX, finalY); - } - - // Checks whether the pads have been dragged enough to trigger a haptic feedback - public bool HapticDragRFauxLizard(double dx, double dy, bool isTouched) - { - if (isTouched) - { - hapticClearedR = false; - - // Timeout check: reset delta if accumulation is too slow - DateTime now = DateTime.UtcNow; - if (hapticBufferCountR == 0) - { - hapticLastTimeR = now; - } - else if ((now - hapticLastTimeR).TotalSeconds > hapticResetTime) - { - hapticDeltaR = 0; - hapticLastTimeR = now; - } - - // If buffer is full, remove array contributions at this slot - if (hapticBufferCountR == hapticBufferSize) - { - int idx = hapticBufferIndexR; - hapticDxFlipCountR -= hapticDxFlipFlagBufferR[idx]; - hapticDyFlipCountR -= hapticDyFlipFlagBufferR[idx]; - if (hapticDxSignBufferR[idx] == 0) hapticDxZeroCountR--; - if (hapticDySignBufferR[idx] == 0) hapticDyZeroCountR--; - hapticMagSumR -= hapticMagBufferR[idx]; - } - else - { - hapticBufferCountR++; - } - - // Compute current signs - int curDxSign = Math.Sign(dx); - int curDySign = Math.Sign(dy); - - // Compute flips against previous signs - int dxFlip = 0, dyFlip = 0; - if (hapticBufferCountR > 1) - { - int prevIndex = (hapticBufferIndexR - 1 + hapticBufferSize) % hapticBufferSize; - int prevDxSign = hapticDxSignBufferR[prevIndex]; - int prevDySign = hapticDySignBufferR[prevIndex]; - - if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; - if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; - } - - // Compute current magnitude - double mag = Math.Sqrt(dx * dx + dy * dy); - - // Store signs and flip flags and magnitude in array for rolling sums - int i = hapticBufferIndexR; - hapticDxSignBufferR[i] = curDxSign; - hapticDySignBufferR[i] = curDySign; - hapticDxFlipFlagBufferR[i] = dxFlip; - hapticDyFlipFlagBufferR[i] = dyFlip; - hapticMagBufferR[i] = mag; - - // Increment rolling sums - if (curDxSign == 0) hapticDxZeroCountR++; - if (curDySign == 0) hapticDyZeroCountR++; - hapticDxFlipCountR += dxFlip; - hapticDyFlipCountR += dyFlip; - hapticMagSumR += mag; - - // Store new buffer index for later - hapticBufferIndexR++; - if (hapticBufferIndexR == hapticBufferSize) hapticBufferIndexR = 0; - - // No need to proceed as buffer is not full yet - if (hapticBufferCountR != hapticBufferSize) - return false; - - // Compute average magnitude over buffer for smoothing - double avgMag = hapticMagSumR / hapticBufferCountR; - - // Calculate a penalty for the magnitude - double bufferSpan = hapticBufferSize - hapticBufferMidpoint; - double invSpan = 1.0 / bufferSpan; - bool dxZeroEdge = hapticDxZeroCountR == hapticBufferCountR; - bool dyZeroEdge = hapticDyZeroCountR == hapticBufferCountR; - double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; - - double dxFactor = dxZeroEdge - ? 0.0 - : (hapticDxFlipCountR < hapticBufferMidpoint - ? factor - : -factor * ((hapticDxFlipCountR - hapticBufferMidpoint) * invSpan)); - - double dyFactor = dyZeroEdge - ? 0.0 - : (hapticDyFlipCountR < hapticBufferMidpoint - ? factor - : -factor * ((hapticDyFlipCountR - hapticBufferMidpoint) * invSpan)); - - // Final penalty - hapticDeltaR += avgMag * (dxFactor + dyFactor); - - if (hapticDeltaR >= hapticTriggerDelta) - { - hapticDeltaR -= Math.Floor(hapticDeltaR / hapticTriggerDelta) * hapticTriggerDelta; - hapticLastTimeR = now; - return true; - } - else if (hapticDeltaR < 0) - { - hapticDeltaR = 0; - } - } - else - { - if (!hapticClearedR) - { - hapticClearedR = true; - - // Reset all buffers when not touched - hapticDeltaR = 0; - hapticBufferCountR = 0; - hapticBufferIndexR = 0; - hapticDxFlipCountR = 0; - hapticDyFlipCountR = 0; - hapticDxZeroCountR = 0; - hapticDyZeroCountR = 0; - hapticMagSumR = 0; - } - } - - return false; - } - - public bool HapticDragLFauxLizard(double dx, double dy, bool isTouched) - { - if (isTouched) - { - hapticClearedL = false; - - // Timeout check: reset delta if accumulation is too slow - DateTime now = DateTime.UtcNow; - if (hapticBufferCountL == 0) - { - hapticLastTimeL = now; - } - else if ((now - hapticLastTimeL).TotalSeconds > hapticResetTime) - { - hapticDeltaL = 0; - hapticLastTimeL = now; - } - - // If buffer is full, remove array contributions at this slot - if (hapticBufferCountL == hapticBufferSize) - { - int idx = hapticBufferIndexL; - hapticDxFlipCountL -= hapticDxFlipFlagBufferL[idx]; - hapticDyFlipCountL -= hapticDyFlipFlagBufferL[idx]; - if (hapticDxSignBufferL[idx] == 0) hapticDxZeroCountL--; - if (hapticDySignBufferL[idx] == 0) hapticDyZeroCountL--; - hapticMagSumL -= hapticMagBufferL[idx]; - } - else - { - hapticBufferCountL++; - } - - // Compute current signs - int curDxSign = Math.Sign(dx); - int curDySign = Math.Sign(dy); - - // Compute flips against previous signs - int dxFlip = 0, dyFlip = 0; - if (hapticBufferCountL > 1) - { - int prevIndex = (hapticBufferIndexL - 1 + hapticBufferSize) % hapticBufferSize; - int prevDxSign = hapticDxSignBufferL[prevIndex]; - int prevDySign = hapticDySignBufferL[prevIndex]; - - if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; - if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; - } - - // Compute current magnitude - double mag = Math.Sqrt(dx * dx + dy * dy); - - // Store signs and flip flags and magnitude in array for rolling sums - int i = hapticBufferIndexL; - hapticDxSignBufferL[i] = curDxSign; - hapticDySignBufferL[i] = curDySign; - hapticDxFlipFlagBufferL[i] = dxFlip; - hapticDyFlipFlagBufferL[i] = dyFlip; - hapticMagBufferL[i] = mag; - - // Increment rolling sums - if (curDxSign == 0) hapticDxZeroCountL++; - if (curDySign == 0) hapticDyZeroCountL++; - hapticDxFlipCountL += dxFlip; - hapticDyFlipCountL += dyFlip; - hapticMagSumL += mag; - - // Store new buffer index for later - hapticBufferIndexL++; - if (hapticBufferIndexL == hapticBufferSize) hapticBufferIndexL = 0; - - // No need to proceed as buffer is not full yet - if (hapticBufferCountL != hapticBufferSize) - return false; - - // Compute average magnitude over buffer for smoothing - double avgMag = hapticMagSumL / hapticBufferCountL; - - // Calculate a penalty for the magnitude - double bufferSpan = hapticBufferSize - hapticBufferMidpoint; - double invSpan = 1.0 / bufferSpan; - bool dxZeroEdge = hapticDxZeroCountL == hapticBufferCountL; - bool dyZeroEdge = hapticDyZeroCountL == hapticBufferCountL; - double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; - - double dxFactor = dxZeroEdge - ? 0.0 - : (hapticDxFlipCountL < hapticBufferMidpoint - ? factor - : -factor * ((hapticDxFlipCountL - hapticBufferMidpoint) * invSpan)); - - double dyFactor = dyZeroEdge - ? 0.0 - : (hapticDyFlipCountL < hapticBufferMidpoint - ? factor - : -factor * ((hapticDyFlipCountL - hapticBufferMidpoint) * invSpan)); - - // Final penalty - hapticDeltaL += avgMag * (dxFactor + dyFactor); - - if (hapticDeltaL >= hapticTriggerDelta) - { - hapticDeltaL -= Math.Floor(hapticDeltaL / hapticTriggerDelta) * hapticTriggerDelta; - hapticLastTimeL = now; - return true; - } - else if (hapticDeltaL < 0) - { - hapticDeltaL = 0; - } - } - else - { - if (!hapticClearedL) - { - hapticClearedL = true; - - // Reset all buffers when not touched - hapticDeltaL = 0; - hapticBufferCountL = 0; - hapticBufferIndexL = 0; - hapticDxFlipCountL = 0; - hapticDyFlipCountL = 0; - hapticDxZeroCountL = 0; - hapticDyZeroCountL = 0; - hapticMagSumL = 0; - } - } - - return false; - } - - // Input smoothing - private double SmoothDelta(double raw, double[] buffer, ref int index, ref int count, ref double sum) - { - if (count < bufferSize) - { - buffer[index] = raw; - sum += raw; - count++; - } - else - { - sum -= buffer[index]; - buffer[index] = raw; - sum += raw; - } - - index++; - if (index == bufferSize) - index = 0; - - return sum / count; - } - - // Velocity smoothing - private double SmoothVelocity(double v, double[] buffer, ref int index, ref int count, ref double sum) - { - if (count < velocityWindowSize) - { - buffer[index] = v; - sum += v; - count++; - } - else - { - sum -= buffer[index]; - buffer[index] = v; - sum += v; - } - - index++; - if (index == velocityWindowSize) - index = 0; - - return sum / count; - } - - // Acceleration ramp - private double ApplyAccelerationRamp(double delta) - { - double steepness = 0.1; - double midpoint = 6.0; - double maxBoost = 2.0; - double baseBoost = 1.7; - - double absDelta = Math.Abs(delta); - double sigmoidBoost = 1.0 + (maxBoost - 1.0) / (1.0 + Math.Exp(-steepness * (absDelta - midpoint))); - return delta * Math.Max(sigmoidBoost, baseBoost); - } - - // Glide decay - private double ApplyReleaseGlide(double velocity, double elapsed) - { - double rampedRate = 3.0 + Math.Pow(elapsed * 7.0, 2); - return velocity * Math.Exp(-elapsed * rampedRate); - } - } -} \ No newline at end of file diff --git a/SteamController/Devices/MouseControllerFauxLizardHaptic.cs b/SteamController/Devices/MouseControllerFauxLizardHaptic.cs new file mode 100644 index 0000000..07f1ace --- /dev/null +++ b/SteamController/Devices/MouseControllerFauxLizardHaptic.cs @@ -0,0 +1,199 @@ +namespace SteamController.Devices +{ + public partial class MouseController + { + // Adjustable parameters (tweakable settings) + private const int HapticBufferSize = 10; // Haptic smoothing and jitter reduction buffer + private const double HapticTriggerDelta = 30; // Distance required for haptic feedback on each pad + private const int HapticResetTime = 5; // Number of seconds to invalidate a haptic feedback, prevents triggering on drag jitter + + // Functional constants (derived once) + private static readonly double HapticBufferMidpoint = HapticBufferSize / 2.0; + + // Runtime state haptics + private struct HapticState + { + public readonly int[] DxSign; + public readonly int[] DySign; + public readonly int[] DxFlipFlag; + public readonly int[] DyFlipFlag; + public readonly double[] Mag; + + public double Delta; + public int BufferCount; + public int BufferIndex; + public int DxFlipCount; + public int DyFlipCount; + public int DxZeroCount; + public int DyZeroCount; + public double MagSum; + public DateTime LastTime; + public bool Cleared; + + public HapticState(int n) + { + DxSign = new int[n]; + DySign = new int[n]; + DxFlipFlag = new int[n]; + DyFlipFlag = new int[n]; + Mag = new double[n]; + + Delta = 0; + BufferCount = 0; + BufferIndex = 0; + DxFlipCount = 0; + DyFlipCount = 0; + DxZeroCount = 0; + DyZeroCount = 0; + MagSum = 0; + LastTime = DateTime.MinValue; + Cleared = true; + } + + public void ResetBuffers() + { + Delta = 0; + BufferCount = 0; + BufferIndex = 0; + DxFlipCount = 0; + DyFlipCount = 0; + DxZeroCount = 0; + DyZeroCount = 0; + MagSum = 0; + } + } + + private HapticState hapticR = new HapticState(HapticBufferSize); + private HapticState hapticL = new HapticState(HapticBufferSize); + + // Checks whether the pads have been dragged enough to trigger a haptic feedback + public bool HapticDragRFauxLizard(double dx, double dy, bool isTouched) + => HapticDragFauxLizard(ref hapticR, dx, dy, isTouched); + + public bool HapticDragLFauxLizard(double dx, double dy, bool isTouched) + => HapticDragFauxLizard(ref hapticL, dx, dy, isTouched); + + private bool HapticDragFauxLizard(ref HapticState s, double dx, double dy, bool isTouched) + { + if (isTouched) + { + s.Cleared = false; + + // Timeout check: reset delta if accumulation is too slow + DateTime now = DateTime.UtcNow; + if (s.BufferCount == 0) + { + s.LastTime = now; + } + else if ((now - s.LastTime).TotalSeconds > HapticResetTime) + { + s.Delta = 0; + s.LastTime = now; + } + + // If buffer is full, remove array contributions at this slot + if (s.BufferCount == HapticBufferSize) + { + int idx = s.BufferIndex; + s.DxFlipCount -= s.DxFlipFlag[idx]; + s.DyFlipCount -= s.DyFlipFlag[idx]; + if (s.DxSign[idx] == 0) s.DxZeroCount--; + if (s.DySign[idx] == 0) s.DyZeroCount--; + s.MagSum -= s.Mag[idx]; + } + else + { + s.BufferCount++; + } + + // Compute current signs + int curDxSign = Math.Sign(dx); + int curDySign = Math.Sign(dy); + + // Compute flips against previous signs + int dxFlip = 0, dyFlip = 0; + if (s.BufferCount > 1) + { + int prevIndex = (s.BufferIndex - 1 + HapticBufferSize) % HapticBufferSize; + int prevDxSign = s.DxSign[prevIndex]; + int prevDySign = s.DySign[prevIndex]; + + if (prevDxSign != 0 && curDxSign != 0 && prevDxSign != curDxSign) dxFlip = 1; + if (prevDySign != 0 && curDySign != 0 && prevDySign != curDySign) dyFlip = 1; + } + + // Compute current magnitude + double mag = Math.Sqrt(dx * dx + dy * dy); + + // Store signs and flip flags and magnitude in array for rolling sums + int i = s.BufferIndex; + s.DxSign[i] = curDxSign; + s.DySign[i] = curDySign; + s.DxFlipFlag[i] = dxFlip; + s.DyFlipFlag[i] = dyFlip; + s.Mag[i] = mag; + + // Increment rolling sums + if (curDxSign == 0) s.DxZeroCount++; + if (curDySign == 0) s.DyZeroCount++; + s.DxFlipCount += dxFlip; + s.DyFlipCount += dyFlip; + s.MagSum += mag; + + // Store new buffer index for later + s.BufferIndex++; + if (s.BufferIndex == HapticBufferSize) s.BufferIndex = 0; + + // No need to proceed as buffer is not full yet + if (s.BufferCount != HapticBufferSize) + return false; + + // Compute average magnitude over buffer for smoothing + double avgMag = s.MagSum / s.BufferCount; + + // Calculate a penalty for the magnitude + double bufferSpan = HapticBufferSize - HapticBufferMidpoint; + double invSpan = 1.0 / bufferSpan; + bool dxZeroEdge = s.DxZeroCount == s.BufferCount; + bool dyZeroEdge = s.DyZeroCount == s.BufferCount; + double factor = (dxZeroEdge || dyZeroEdge) ? 1.0 : 0.5; + + double dxFactor = dxZeroEdge + ? 0.0 + : (s.DxFlipCount < HapticBufferMidpoint + ? factor + : -factor * ((s.DxFlipCount - HapticBufferMidpoint) * invSpan)); + + double dyFactor = dyZeroEdge + ? 0.0 + : (s.DyFlipCount < HapticBufferMidpoint + ? factor + : -factor * ((s.DyFlipCount - HapticBufferMidpoint) * invSpan)); + + // Final penalty + s.Delta += avgMag * (dxFactor + dyFactor); + + if (s.Delta >= HapticTriggerDelta) + { + s.Delta -= Math.Floor(s.Delta / HapticTriggerDelta) * HapticTriggerDelta; + s.LastTime = now; + return true; + } + else if (s.Delta < 0) + { + s.Delta = 0; + } + } + else + { + if (!s.Cleared) + { + s.Cleared = true; + s.ResetBuffers(); + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/SteamController/Devices/MouseControllerFauxLizardMove.cs b/SteamController/Devices/MouseControllerFauxLizardMove.cs new file mode 100644 index 0000000..cf76d6f --- /dev/null +++ b/SteamController/Devices/MouseControllerFauxLizardMove.cs @@ -0,0 +1,198 @@ +namespace SteamController.Devices +{ + public partial class MouseController + { + // Adjustable parameters (tweakable settings) + private const int BufferSize = 4; // Input smoothing buffer size + private const int GestureRadius = 30; // Gesture commit threshold (pixels) + private const double GestureFlushRate = 0.1; // Rate at which gesture flush decays + private const int VelocityWindowSize = 20; // Velocity smoothing window size + private const double MinGlideMagnitude = 0.8; // Minimum velocity to start glide + private const double MinGlideVelocity = 0.15; // Minimum velocity to continue glide + + // Functional constants (derived once) + private static readonly double GestureRadiusSq = GestureRadius * GestureRadius; + private static readonly double MinGlideMagnitudeSq = MinGlideMagnitude * MinGlideMagnitude; + private static readonly double MinGlideVelocitySq = MinGlideVelocity * MinGlideVelocity; + + // Runtime state RPad (movement) + private readonly double[] bufX = new double[BufferSize]; + private int bufXIndex = 0, bufXCount = 0; + private double bufXSum = 0; + + private readonly double[] bufY = new double[BufferSize]; + private int bufYIndex = 0, bufYCount = 0; + private double bufYSum = 0; + + private double totalDeltaX = 0, totalDeltaY = 0; + private bool gestureCommitted = false; + private double gestureFlushX = 0, gestureFlushY = 0; + + private bool isGliding = false; + private bool wasTouched = false; + private long releaseTicks; + private double releaseVelocityX = 0, releaseVelocityY = 0; + + private readonly double[] velocityX = new double[VelocityWindowSize]; + private int velocityXIndex = 0, velocityXCount = 0; + private double velocityXSum = 0; + + private readonly double[] velocityY = new double[VelocityWindowSize]; + private int velocityYIndex = 0, velocityYCount = 0; + private double velocityYSum = 0; + + // Main movement logic + public void MoveByFauxLizard(double dx, double dy, bool isTouched) + { + if (!isTouched) + { + // Start glide if gesture was committed and velocity is high enough + if (!isGliding && gestureCommitted) + { + double magSq = releaseVelocityX * releaseVelocityX + releaseVelocityY * releaseVelocityY; + if (magSq >= MinGlideMagnitudeSq) + { + releaseTicks = System.Diagnostics.Stopwatch.GetTimestamp(); + isGliding = true; + } + } + + // Continue glide if active + if (isGliding) + { + double elapsed = + (System.Diagnostics.Stopwatch.GetTimestamp() - releaseTicks) + / (double)System.Diagnostics.Stopwatch.Frequency; + double glideX = ApplyReleaseGlide(releaseVelocityX, elapsed); + double glideY = ApplyReleaseGlide(releaseVelocityY, elapsed); + + double glideMagSq = glideX * glideX + glideY * glideY; + if (glideMagSq < MinGlideVelocitySq) + { + isGliding = false; + releaseVelocityX = releaseVelocityY = 0; + } + else + { + MoveBy(glideX, glideY); + } + } + + // Reset gesture state ONCE on touch release + if (wasTouched) + { + ResetFauxLizardGesture(); + wasTouched = false; + } + + return; + } + + // Touch just started + wasTouched = true; + isGliding = false; + + // Smooth input deltas + double smoothedX = SmoothRing(dx, bufX, ref bufXIndex, ref bufXCount, ref bufXSum); + double smoothedY = SmoothRing(dy, bufY, ref bufYIndex, ref bufYCount, ref bufYSum); + + // Apply acceleration ramp + double rampedX = ApplyAccelerationRamp(smoothedX); + double rampedY = ApplyAccelerationRamp(smoothedY); + + // Accumulate gesture until committed + if (!gestureCommitted) + { + totalDeltaX += rampedX; + totalDeltaY += rampedY; + + double gestureMagSq = totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY; + if (gestureMagSq > GestureRadiusSq) + { + gestureCommitted = true; + gestureFlushX = totalDeltaX; + gestureFlushY = totalDeltaY; + + velocityXIndex = velocityXCount = 0; velocityXSum = 0; + velocityYIndex = velocityYCount = 0; velocityYSum = 0; + } + else return; + } + + // Track velocity using rolling sum + releaseVelocityX = SmoothRing(rampedX, velocityX, ref velocityXIndex, ref velocityXCount, ref velocityXSum); + releaseVelocityY = SmoothRing(rampedY, velocityY, ref velocityYIndex, ref velocityYCount, ref velocityYSum); + + // Apply gesture flush + double flushedX = gestureFlushX * GestureFlushRate; + double flushedY = gestureFlushY * GestureFlushRate; + gestureFlushX -= flushedX; + gestureFlushY -= flushedY; + + // Final movement vector + double finalX = flushedX + rampedX; + double finalY = flushedY + rampedY; + + MoveBy(finalX, finalY); + } + + // Buffer smoothing helper + private static double SmoothRing(double v, double[] buffer, ref int index, ref int count, ref double sum) + { + int window = buffer.Length; + + if (count < window) + { + buffer[index] = v; + sum += v; + count++; + } + else + { + sum -= buffer[index]; + buffer[index] = v; + sum += v; + } + + index++; + if (index == window) + index = 0; + + return sum / count; + } + + // Acceleration ramp + private double ApplyAccelerationRamp(double delta) + { + double steepness = 0.1; + double midpoint = 6.0; + double maxBoost = 2.0; + double baseBoost = 1.7; + + double absDelta = Math.Abs(delta); + double sigmoidBoost = 1.0 + (maxBoost - 1.0) / (1.0 + Math.Exp(-steepness * (absDelta - midpoint))); + return delta * Math.Max(sigmoidBoost, baseBoost); + } + + // Glide decay + private double ApplyReleaseGlide(double velocity, double elapsed) + { + double rampedRate = 3.0 + Math.Pow(elapsed * 7.0, 2); + return velocity * Math.Exp(-elapsed * rampedRate); + } + + //Resets all values for a new touch + private void ResetFauxLizardGesture() + { + gestureCommitted = false; + + bufXIndex = bufXCount = 0; bufXSum = 0; + bufYIndex = bufYCount = 0; bufYSum = 0; + + velocityXIndex = velocityXCount = 0; velocityXSum = 0; + velocityYIndex = velocityYCount = 0; velocityYSum = 0; + + totalDeltaX = totalDeltaY = 0; + } + } +} \ No newline at end of file From 68b620971f0a596101f2c40a9b798545c9adce0f Mon Sep 17 00:00:00 2001 From: General4878 Date: Sun, 28 Dec 2025 17:10:32 +0100 Subject: [PATCH 11/11] FLM: Changed application priority to above normal, and ThreadLoop priority to highest to make the controller app more responsive. --- SteamController/ContextThread.cs | 7 +++++++ SteamController/Program.cs | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/SteamController/ContextThread.cs b/SteamController/ContextThread.cs index 0362ed8..d11c0df 100644 --- a/SteamController/ContextThread.cs +++ b/SteamController/ContextThread.cs @@ -31,6 +31,13 @@ namespace SteamController ThreadSleep((int)startDelayMs); } + //Raise the thread priority to High for better responsiveness + try + { + Thread.CurrentThread.Priority = ThreadPriority.Highest; + } + catch { } + var stopwatch = new Stopwatch(); stopwatch.Start(); int updates = 0; diff --git a/SteamController/Program.cs b/SteamController/Program.cs index f7a9798..9d7cde7 100644 --- a/SteamController/Program.cs +++ b/SteamController/Program.cs @@ -1,4 +1,5 @@ using CommonHelpers; +using System.Diagnostics; namespace SteamController { @@ -12,6 +13,13 @@ namespace SteamController { Instance.WithSentry(() => { + // Raise the application priority to Above Normal for better responsiveness + try + { + Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.AboveNormal; + } + catch { } + // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize();