This commit is contained in:
General4878 2025-12-28 16:10:42 +00:00 committed by GitHub
commit da712e721a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 483 additions and 2 deletions

View file

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

View file

@ -2,7 +2,7 @@ using WindowsInput;
namespace SteamController.Devices
{
public class MouseController : IDisposable
public partial class MouseController : IDisposable
{
private struct Accum
{

View file

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

View file

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

View file

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

View file

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

View file

@ -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<bool>("FauxLizardMode", false); }
set { Set("FauxLizardMode", value); }
}
public override string ToString()
{
return "";