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/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/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 diff --git a/SteamController/Profiles/Default/GuideShortcutsProfile.cs b/SteamController/Profiles/Default/GuideShortcutsProfile.cs index 6a88c41..be29c27 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 { @@ -140,6 +141,31 @@ namespace SteamController.Profiles.Default ) ); } + + if (SettingsDebug.Default.FauxLizardMode && !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) @@ -174,7 +200,43 @@ namespace SteamController.Profiles.Default c.Mouse[Devices.MouseController.Button.Left] = c.Steam.BtnRPadPress; } - if (c.Steam.RPadX || c.Steam.RPadY) + bool simpleEmulation = true; + + 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()) + { + c.Steam.SendHaptic(HapticPad.Right, HapticStyle.Strong, 8); + } + + // 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); + + // 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), 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(); 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 "";