From c9ee4da7a48d5ec876999f86fcab67d9bc94bcd1 Mon Sep 17 00:00:00 2001 From: maarvin Date: Sat, 7 Feb 2026 13:30:50 +0100 Subject: [PATCH 1/3] Add `double` variant for `Spring` class (#6706) * Add double variant for spring class * Fix precedence implicit brackets --------- Co-authored-by: marvin Co-authored-by: Dean Herbert --- osu.Framework/Graphics/Transforms/Spring.cs | 24 +++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Graphics/Transforms/Spring.cs b/osu.Framework/Graphics/Transforms/Spring.cs index 8fe123a5f..9759d7238 100644 --- a/osu.Framework/Graphics/Transforms/Spring.cs +++ b/osu.Framework/Graphics/Transforms/Spring.cs @@ -117,10 +117,18 @@ namespace osu.Framework.Graphics.Transforms protected void ComputeSingleValue(float dt, ref float current, ref float velocity, float target, float targetVelocity) { - float k2Stable = MathF.Max(MathF.Max(k2, dt * dt / 2 + dt * k1 / 2), dt * k1); + float k2Stable = MathF.Max(MathF.Max(k2, (dt * dt) / 2 + (dt * k1) / 2), dt * k1); current += dt * velocity; - velocity += (dt * (target + k3 * targetVelocity - current - k1 * velocity)) / k2Stable; + velocity += (dt * (target + (k3 * targetVelocity) - current - (k1 * velocity))) / k2Stable; + } + + protected void ComputeSingleValue(float dt, ref double current, ref double velocity, double target, double targetVelocity) + { + float k2Stable = MathF.Max(MathF.Max(k2, (dt * dt) / 2 + (dt * k1) / 2), dt * k1); + + current += dt * velocity; + velocity += (dt * (target + (k3 * targetVelocity) - current - (k1 * velocity))) / k2Stable; } } @@ -136,6 +144,18 @@ namespace osu.Framework.Graphics.Transforms } } + public class DoubleSpring : Spring + { + protected override double GetTargetVelocity(double target, double previousTarget, float dt) => (target - previousTarget) / dt; + + protected override double ComputeNextValue(float dt, double target, double targetVelocity) + { + ComputeSingleValue(dt, ref Current, ref Velocity, target, targetVelocity); + + return Current; + } + } + public class Vector2Spring : Spring { protected override Vector2 GetTargetVelocity(Vector2 target, Vector2 previousTarget, float dt) => (target - previousTarget) / dt; From ec800bc9c760b9d3bea0a0676850296457175db2 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Sun, 8 Feb 2026 17:53:13 +0100 Subject: [PATCH 2/3] Fix unplugging controller sometimes crashing game (#6692) Could crash if the ID wasn't in the dictionary. The old code also didn't properly close everything (it would close either the jostick or the gamepad, but not both). --- .../Platform/SDL2/SDL2Window_Input.cs | 24 ++++++++++++------- .../Platform/SDL3/SDL3Window_Input.cs | 22 ++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs index b332546ee..6c164e28e 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs @@ -275,8 +275,7 @@ namespace osu.Framework.Platform.SDL2 break; case SDL_EventType.SDL_CONTROLLERDEVICEREMOVED: - SDL_GameControllerClose(controllers[evtCdevice.which].ControllerHandle); - controllers.Remove(evtCdevice.which); + removeJoystick(evtCdevice.which); break; case SDL_EventType.SDL_CONTROLLERDEVICEREMAPPED: @@ -323,6 +322,20 @@ namespace osu.Framework.Platform.SDL2 controllers[instanceID] = new SDL2ControllerBindings(joystick, controller); } + private void removeJoystick(int which) + { + int instanceID = SDL_JoystickGetDeviceInstanceID(which); + + if (controllers.Remove(instanceID, out var controller)) + { + if (controller.ControllerHandle != IntPtr.Zero) + SDL_GameControllerClose(controller.ControllerHandle); + + if (controller.JoystickHandle != IntPtr.Zero) + SDL_JoystickClose(controller.JoystickHandle); + } + } + /// /// Populates with joysticks that are already connected. /// @@ -343,12 +356,7 @@ namespace osu.Framework.Platform.SDL2 break; case SDL_EventType.SDL_JOYDEVICEREMOVED: - // if the joystick is already closed, ignore it - if (!controllers.ContainsKey(evtJdevice.which)) - break; - - SDL_JoystickClose(controllers[evtJdevice.which].JoystickHandle); - controllers.Remove(evtJdevice.which); + removeJoystick(evtJdevice.which); break; } } diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs index 4f10522e6..e06f6efb0 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -324,8 +324,7 @@ namespace osu.Framework.Platform.SDL3 break; case SDL_EventType.SDL_EVENT_GAMEPAD_REMOVED: - SDL_CloseGamepad(controllers[evtCdevice.which].GamepadHandle); - controllers.Remove(evtCdevice.which); + removeJoystick(evtCdevice.which); break; case SDL_EventType.SDL_EVENT_GAMEPAD_REMAPPED: @@ -370,6 +369,18 @@ namespace osu.Framework.Platform.SDL3 controllers[instanceID] = new SDL3ControllerBindings(joystick, controller); } + private void removeJoystick(SDL_JoystickID instanceID) + { + if (controllers.Remove(instanceID, out var controller)) + { + if (controller.GamepadHandle != null) + SDL_CloseGamepad(controller.GamepadHandle); + + if (controller.JoystickHandle != null) + SDL_CloseJoystick(controller.JoystickHandle); + } + } + /// /// Populates with joysticks that are already connected. /// @@ -395,12 +406,7 @@ namespace osu.Framework.Platform.SDL3 break; case SDL_EventType.SDL_EVENT_JOYSTICK_REMOVED: - // if the joystick is already closed, ignore it - if (!controllers.ContainsKey(evtJdevice.which)) - break; - - SDL_CloseJoystick(controllers[evtJdevice.which].JoystickHandle); - controllers.Remove(evtJdevice.which); + removeJoystick(evtJdevice.which); break; } } From afd0a6ffc8a1c386b4a2f52b95dc648ed98d4c74 Mon Sep 17 00:00:00 2001 From: YNataniel295 <72387295+YNataniel295@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:58:37 +0200 Subject: [PATCH 3/3] Fix hovering with tablet causing mouse buttons to be immediately released (#6701) * Fix LMB hold when hovering with tablet pen Fixes an issue where LMB would be released immediately when the pen is in hover range. Added pen pressure hysteresis, may be needed. * Fix LMB hold when hovering with tablet pen Fixes an issue where LMB would be released immediately when the pen is in hover range. Added pen pressure hysteresis, may be needed. * Fix LMB hold when hovering with tablet pen Stop continuously propagating pen LMB state; use edge-only press/release with a small internal hysteresis and release on tablet disconnect. * Fix LMB hold when hovering with tablet pen * Fix LMB hold when hovering with tablet pen * Rewrite for simplicity --------- Co-authored-by: Dean Herbert --- .../Tablet/OpenTabletDriverHandler.cs | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs b/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs index 99a983786..dcbe4c045 100644 --- a/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs +++ b/osu.Framework/Input/Handlers/Tablet/OpenTabletDriverHandler.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -44,7 +45,7 @@ namespace osu.Framework.Input.Handlers.Tablet public Bindable Rotation { get; } = new Bindable(); - public BindableFloat PressureThreshold { get; } = new BindableFloat(0.0f) + public BindableFloat PressureThreshold { get; } = new BindableFloat { MinValue = 0f, MaxValue = 1f, @@ -119,7 +120,34 @@ namespace osu.Framework.Input.Handlers.Tablet enqueueInput(new MousePositionRelativeInputFromPen { Delta = new Vector2(delta.X, delta.Y), DeviceType = lastTabletDeviceType }); } - void IPressureHandler.SetPressure(float percentage) => enqueueInput(new MouseButtonInputFromPen(percentage > PressureThreshold.Value) { DeviceType = lastTabletDeviceType }); + private bool penPressed; + + void IPressureHandler.SetPressure(float pressure) + { + // Most important for edge cases where users have pressure set to 0 or 1 and tablets can report fuzzy data. + const float hysteresis_half = 0.02f; + + pressure = Math.Clamp(pressure, 0f, 1f); + + float releaseThreshold = PressureThreshold.Value - hysteresis_half; + float pressThreshold = PressureThreshold.Value + hysteresis_half; + + // keep press..release threshold range constant for edge cases. + if (releaseThreshold < 0f) + { + pressThreshold = hysteresis_half * 2; + releaseThreshold = 0f; + } + else if (pressThreshold > 1f) + { + releaseThreshold = 1 - (hysteresis_half * 2); + pressThreshold = 1f; + } + + setPressed(penPressed + ? pressure > releaseThreshold + : pressure > pressThreshold); + } private void handleTabletsChanged(object? sender, IEnumerable tablets) { @@ -135,7 +163,22 @@ namespace osu.Framework.Input.Handlers.Tablet updateOutputArea(host.Window); } else + { + // Ensure we don't leave the simulated mouse button pressed if the tablet disappears. + setPressed(false); tablet.Value = null; + } + } + + private void setPressed(bool pressed) + { + // Importantly, only fire input when the state changes. + // If we fire more often, this may intefere with users that click with mouse but use tablet for positional input (hovering). + if (pressed == penPressed) + return; + + enqueueInput(new MouseButtonInputFromPen(pressed) { DeviceType = lastTabletDeviceType }); + penPressed = pressed; } private void handleDeviceReported(object? sender, IDeviceReport report)