From ee8a2236f2ed31db788acbe23d983f75134df288 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Mar 2024 10:03:38 +0300 Subject: [PATCH 001/140] Change `PassThroughInputManager` to not sync new presses in `Sync()` except for initial state --- .../Input/PassThroughInputManager.cs | 132 ++++++++++++------ 1 file changed, 86 insertions(+), 46 deletions(-) diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index 5856141d71..cb8c11b883 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -94,11 +94,6 @@ protected override bool Handle(UIEvent e) switch (e) { - case MouseMoveEvent mouseMove: - if (mouseMove.ScreenSpaceMousePosition != CurrentState.Mouse.Position) - new MousePositionAbsoluteInput { Position = mouseMove.ScreenSpaceMousePosition }.Apply(CurrentState, this); - break; - case MouseDownEvent mouseDown: // safe-guard for edge cases. if (!CurrentState.Mouse.IsPressed(mouseDown.Button)) @@ -111,24 +106,63 @@ protected override bool Handle(UIEvent e) new MouseButtonInput(mouseUp.Button, false).Apply(CurrentState, this); break; + case MouseMoveEvent mouseMove: + if (mouseMove.ScreenSpaceMousePosition != CurrentState.Mouse.Position) + new MousePositionAbsoluteInput { Position = mouseMove.ScreenSpaceMousePosition }.Apply(CurrentState, this); + break; + case ScrollEvent scroll: new MouseScrollRelativeInput { Delta = scroll.ScrollDelta, IsPrecise = scroll.IsPrecise }.Apply(CurrentState, this); break; + case KeyDownEvent keyDown: + if (!keyDown.Repeat) + new KeyboardKeyInput(keyDown.Key, true).Apply(CurrentState, this); + + break; + + case KeyUpEvent keyUp: + new KeyboardKeyInput(keyUp.Key, false).Apply(CurrentState, this); + break; + case TouchEvent touch: new TouchInput(touch.ScreenSpaceTouch, touch.IsActive(touch.ScreenSpaceTouch)).Apply(CurrentState, this); break; - case MidiEvent midi: - new MidiKeyInput(midi.Key, midi.Velocity, midi.IsPressed(midi.Key)).Apply(CurrentState, this); + case JoystickPressEvent joystickPress: + new JoystickButtonInput(joystickPress.Button, true).Apply(CurrentState, this); break; - case KeyboardEvent: - case JoystickButtonEvent: - case JoystickAxisMoveEvent: - case TabletPenButtonEvent: - case TabletAuxiliaryButtonEvent: - SyncInputState(e.CurrentState); + case JoystickReleaseEvent joystickRelease: + new JoystickButtonInput(joystickRelease.Button, false).Apply(CurrentState, this); + break; + + case JoystickAxisMoveEvent joystickAxisMove: + new JoystickAxisInput(joystickAxisMove.Axis).Apply(CurrentState, this); + break; + + case MidiDownEvent midiDown: + new MidiKeyInput(midiDown.Key, midiDown.Velocity, true).Apply(CurrentState, this); + break; + + case MidiUpEvent midiUp: + new MidiKeyInput(midiUp.Key, midiUp.Velocity, false).Apply(CurrentState, this); + break; + + case TabletPenButtonPressEvent tabletPenButtonPress: + new TabletPenButtonInput(tabletPenButtonPress.Button, true).Apply(CurrentState, this); + break; + + case TabletPenButtonReleaseEvent tabletPenButtonRelease: + new TabletPenButtonInput(tabletPenButtonRelease.Button, false).Apply(CurrentState, this); + break; + + case TabletAuxiliaryButtonPressEvent tabletAuxiliaryButtonPress: + new TabletAuxiliaryButtonInput(tabletAuxiliaryButtonPress.Button, true).Apply(CurrentState, this); + break; + + case TabletAuxiliaryButtonReleaseEvent tabletAuxiliaryButtonPress: + new TabletAuxiliaryButtonInput(tabletAuxiliaryButtonPress.Button, true).Apply(CurrentState, this); break; } @@ -140,7 +174,7 @@ protected override bool Handle(UIEvent e) protected override void LoadComplete() { base.LoadComplete(); - Sync(); + Sync(applyPresses: true); } protected override void Update() @@ -156,46 +190,52 @@ protected override void Update() /// Call this when parent changed somehow. /// /// If this is false, assume parent input manager is unchanged from before. - public void Sync(bool useCachedParentInputManager = false) + /// Whether to also apply press inputs for buttons that are shown to be pressed in the parent input manager. + public void Sync(bool useCachedParentInputManager = false, bool applyPresses = false) { if (!UseParentInput) return; if (!useCachedParentInputManager) parentInputManager = GetContainingInputManager(); - SyncInputState(parentInputManager?.CurrentState); + SyncInputState(parentInputManager?.CurrentState, applyPresses); } /// /// Sync current state to a certain state. /// /// The state to synchronise current with. If this is null, it is regarded as an empty state. - protected virtual void SyncInputState(InputState state) + /// Whether to also apply press inputs for buttons that are shown to be pressed in the parent input manager. + protected virtual void SyncInputState(InputState state, bool applyPresses) { - // invariant: if mouse button is currently pressed, then it has been pressed in parent (but not the converse) - // therefore, mouse up events are always synced from parent - // mouse down events are not synced to prevent false clicks var mouseDiff = (state?.Mouse?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Mouse.Buttons); + var keyDiff = (state?.Keyboard.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Keyboard.Keys); + var touchDiff = (state?.Touch ?? new TouchState()).EnumerateDifference(CurrentState.Touch); + var joyButtonDiff = (state?.Joystick?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Joystick.Buttons); + var midiDiff = (state?.Midi?.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Midi.Keys); + var tabletPenDiff = (state?.Tablet?.PenButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.PenButtons); + var tabletAuxiliaryDiff = (state?.Tablet?.AuxiliaryButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons); + if (mouseDiff.Released.Length > 0) new MouseButtonInput(mouseDiff.Released.Select(button => new ButtonInputEntry(button, false))).Apply(CurrentState, this); - var keyDiff = (state?.Keyboard.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Keyboard.Keys); foreach (var key in keyDiff.Released) new KeyboardKeyInput(key, false).Apply(CurrentState, this); - foreach (var key in keyDiff.Pressed) - new KeyboardKeyInput(key, true).Apply(CurrentState, this); - var touchDiff = (state?.Touch ?? new TouchState()).EnumerateDifference(CurrentState.Touch); if (touchDiff.deactivated.Length > 0) new TouchInput(touchDiff.deactivated, false).Apply(CurrentState, this); - if (touchDiff.activated.Length > 0) - new TouchInput(touchDiff.activated, true).Apply(CurrentState, this); - var joyButtonDiff = (state?.Joystick?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Joystick.Buttons); foreach (var button in joyButtonDiff.Released) new JoystickButtonInput(button, false).Apply(CurrentState, this); - foreach (var button in joyButtonDiff.Pressed) - new JoystickButtonInput(button, true).Apply(CurrentState, this); + + foreach (var key in midiDiff.Released) + new MidiKeyInput(key, state?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, false).Apply(CurrentState, this); + + foreach (var button in tabletPenDiff.Released) + new TabletPenButtonInput(button, false).Apply(CurrentState, this); + + foreach (var button in tabletAuxiliaryDiff.Released) + new TabletAuxiliaryButtonInput(button, false).Apply(CurrentState, this); // Basically only perform the full state diff if we have found that any axis changed. // This avoids unnecessary alloc overhead. @@ -208,23 +248,23 @@ protected virtual void SyncInputState(InputState state) } } - var midiDiff = (state?.Midi?.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Midi.Keys); - foreach (var key in midiDiff.Released) - new MidiKeyInput(key, state?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, false).Apply(CurrentState, this); - foreach (var key in midiDiff.Pressed) - new MidiKeyInput(key, state?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, true).Apply(CurrentState, this); - - var tabletPenDiff = (state?.Tablet?.PenButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.PenButtons); - foreach (var button in tabletPenDiff.Released) - new TabletPenButtonInput(button, false).Apply(CurrentState, this); - foreach (var button in tabletPenDiff.Pressed) - new TabletPenButtonInput(button, true).Apply(CurrentState, this); - - var tabletAuxiliaryDiff = (state?.Tablet?.AuxiliaryButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons); - foreach (var button in tabletAuxiliaryDiff.Released) - new TabletAuxiliaryButtonInput(button, false).Apply(CurrentState, this); - foreach (var button in tabletAuxiliaryDiff.Pressed) - new TabletAuxiliaryButtonInput(button, true).Apply(CurrentState, this); + if (applyPresses) + { + if (mouseDiff.Pressed.Length > 0) + new MouseButtonInput(mouseDiff.Pressed.Select(button => new ButtonInputEntry(button, true))).Apply(CurrentState, this); + foreach (var key in keyDiff.Pressed) + new KeyboardKeyInput(key, true).Apply(CurrentState, this); + if (touchDiff.activated.Length > 0) + new TouchInput(touchDiff.activated, true).Apply(CurrentState, this); + foreach (var button in joyButtonDiff.Pressed) + new JoystickButtonInput(button, true).Apply(CurrentState, this); + foreach (var key in midiDiff.Pressed) + new MidiKeyInput(key, state?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, true).Apply(CurrentState, this); + foreach (var button in tabletPenDiff.Pressed) + new TabletPenButtonInput(button, true).Apply(CurrentState, this); + foreach (var button in tabletAuxiliaryDiff.Pressed) + new TabletAuxiliaryButtonInput(button, true).Apply(CurrentState, this); + } } } } From 913ad30ded4787985882877660516738d2e2f0d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Mar 2024 10:31:27 +0300 Subject: [PATCH 002/140] Add flag to decide behaviour --- osu.Framework/Input/PassThroughInputManager.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index cb8c11b883..0ce65c9fb7 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -48,6 +48,12 @@ public virtual bool UseParentInput private bool useParentInput = true; + /// + /// Whether to synchronise newly pressed buttons on . + /// Pressed buttons are still synchronised at the point of loading the input manager. + /// + public bool SyncNewPresses { get; init; } = true; + public override bool HandleHoverEvents => UseParentInput ? parentInputManager.HandleHoverEvents : base.HandleHoverEvents; internal override bool BuildNonPositionalInputQueue(List queue, bool allowBlocking = true) @@ -218,22 +224,16 @@ protected virtual void SyncInputState(InputState state, bool applyPresses) if (mouseDiff.Released.Length > 0) new MouseButtonInput(mouseDiff.Released.Select(button => new ButtonInputEntry(button, false))).Apply(CurrentState, this); - foreach (var key in keyDiff.Released) new KeyboardKeyInput(key, false).Apply(CurrentState, this); - if (touchDiff.deactivated.Length > 0) new TouchInput(touchDiff.deactivated, false).Apply(CurrentState, this); - foreach (var button in joyButtonDiff.Released) new JoystickButtonInput(button, false).Apply(CurrentState, this); - foreach (var key in midiDiff.Released) new MidiKeyInput(key, state?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, false).Apply(CurrentState, this); - foreach (var button in tabletPenDiff.Released) new TabletPenButtonInput(button, false).Apply(CurrentState, this); - foreach (var button in tabletAuxiliaryDiff.Released) new TabletAuxiliaryButtonInput(button, false).Apply(CurrentState, this); @@ -248,7 +248,7 @@ protected virtual void SyncInputState(InputState state, bool applyPresses) } } - if (applyPresses) + if (SyncNewPresses || applyPresses) { if (mouseDiff.Pressed.Length > 0) new MouseButtonInput(mouseDiff.Pressed.Select(button => new ButtonInputEntry(button, true))).Apply(CurrentState, this); From bb19c39d6da96e9e5800d4c2b059922252f4f335 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Mar 2024 12:24:55 +0300 Subject: [PATCH 003/140] Do not sync mouse button presses regardless of `SyncNewPresses` --- .../Input/TestScenePassThroughInputManager.cs | 116 +++++++++++++++--- .../Input/PassThroughInputManager.cs | 11 +- 2 files changed, 111 insertions(+), 16 deletions(-) diff --git a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs index 2f01d18b3c..2f189307e0 100644 --- a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs +++ b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs @@ -31,11 +31,11 @@ public TestScenePassThroughInputManager() private ButtonStates keyboard; private ButtonStates joystick; - private void addTestInputManagerStep() + private void addTestInputManagerStep(bool syncNewPresses = true) { AddStep("Add InputManager", () => { - testInputManager = new TestInputManager(); + testInputManager = new TestInputManager { SyncNewPresses = syncNewPresses }; Add(testInputManager); state = testInputManager.CurrentState; mouse = state.Mouse.Buttons; @@ -57,7 +57,7 @@ public void ReceiveInitialState() AddStep("Press A", () => InputManager.PressKey(Key.A)); AddStep("Press Joystick", () => InputManager.PressJoystickButton(JoystickButton.Button1)); addTestInputManagerStep(); - AddAssert("mouse left not pressed", () => !mouse.IsPressed(MouseButton.Left)); + AddAssert("mouse left pressed", () => mouse.IsPressed(MouseButton.Left)); AddAssert("A pressed", () => keyboard.IsPressed(Key.A)); AddAssert("Joystick pressed", () => joystick.IsPressed(JoystickButton.Button1)); AddStep("Release", () => @@ -109,17 +109,17 @@ public void TestUpReceivedOnDownFromSync() } [Test] - public void MouseDownNoSync() + public void TestMouseInput() { addTestInputManagerStep(); AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); AddStep("Press left", () => InputManager.PressButton(MouseButton.Left)); AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("not pressed", () => !mouse.IsPressed(MouseButton.Left)); + AddAssert("pressed", () => mouse.IsPressed(MouseButton.Left)); } [Test] - public void NoMouseUp() + public void TestNoMouseUp() { addTestInputManagerStep(); AddStep("Press left", () => InputManager.PressButton(MouseButton.Left)); @@ -180,6 +180,30 @@ public void TestMidiInput() testInputManager.CurrentState.Midi.Velocities[MidiKey.FSharp3] == 65); } + [Test] + public void TestTabletButtonInput() + { + addTestInputManagerStep(); + + AddStep("press primary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Primary)); + AddStep("press auxiliary button 4", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); + + AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); + + AddStep("release primary pen button", () => InputManager.ReleaseTabletPenButton(TabletPenButton.Primary)); + AddStep("press tertiary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Tertiary)); + AddStep("release auxiliary button 4", () => InputManager.ReleaseTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); + AddStep("press auxiliary button 2", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button2)); + + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); + AddAssert("pen buttons synced properly", () => + !testInputManager.CurrentState.Tablet.PenButtons.Contains(TabletPenButton.Primary) + && testInputManager.CurrentState.Tablet.PenButtons.Contains(TabletPenButton.Tertiary)); + AddAssert("auxiliary buttons synced properly", () => + !testInputManager.CurrentState.Tablet.AuxiliaryButtons.Contains(TabletAuxiliaryButton.Button4) + && testInputManager.CurrentState.Tablet.AuxiliaryButtons.Contains(TabletAuxiliaryButton.Button2)); + } + [Test] public void TestMouseTouchProductionOnPassThrough() { @@ -218,9 +242,77 @@ public void TestMouseTouchProductionOnPassThrough() } [Test] - public void TestTabletButtonInput() + public void TestKeyInput_DisabledSyncNewPresses() { - addTestInputManagerStep(); + addTestInputManagerStep(false); + AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); + AddStep("press keyboard", () => InputManager.PressKey(Key.A)); + AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); + AddAssert("key not pressed", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); + AddAssert("mouse not pressed", () => !testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed); + + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); + AddAssert("key still not pressed", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); + AddAssert("mouse still not pressed", () => !testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed); + + AddStep("release keyboard", () => InputManager.ReleaseKey(Key.A)); + AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); + AddAssert("key still not pressed", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); + AddAssert("mouse still not pressed", () => !testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed); + } + + [Test] + public void TestMouseInput_DisabledSyncNewPresses() + { + addTestInputManagerStep(false); + AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); + AddStep("Press left", () => InputManager.PressButton(MouseButton.Left)); + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); + AddAssert("not pressed", () => !mouse.IsPressed(MouseButton.Left)); + } + + [Test] + public void TestTouchInput_DisabledSyncNewPresses() + { + addTestInputManagerStep(false); + AddStep("begin first touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); + AddAssert("synced properly", () => + testInputManager.CurrentState.Touch.ActiveSources.Single() == TouchSource.Touch1 && + testInputManager.CurrentState.Touch.TouchPositions[(int)TouchSource.Touch1] == Vector2.Zero); + + AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); + AddStep("end first touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); + AddStep("begin second touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, Vector2.One))); + + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); + AddAssert("synced release only", () => !testInputManager.CurrentState.Touch.ActiveSources.HasAnyButtonPressed); + + AddStep("end second touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, new Vector2(2)))); + AddAssert("synced release only", () => !testInputManager.CurrentState.Touch.ActiveSources.HasAnyButtonPressed); + } + + [Test] + public void TestMidiInput_DisabledSyncNewPresses() + { + addTestInputManagerStep(false); + + AddStep("press C3", () => InputManager.PressMidiKey(MidiKey.C3, 70)); + AddAssert("synced properly", () => + testInputManager.CurrentState.Midi.Keys.IsPressed(MidiKey.C3) + && testInputManager.CurrentState.Midi.Velocities[MidiKey.C3] == 70); + + AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); + AddStep("release C3", () => InputManager.ReleaseMidiKey(MidiKey.C3, 40)); + AddStep("press F#3", () => InputManager.PressMidiKey(MidiKey.FSharp3, 65)); + + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); + AddAssert("synced release only", () => !testInputManager.CurrentState.Midi.Keys.HasAnyButtonPressed); + } + + [Test] + public void TestTabletButtonInput_DisabledSyncNewPresses() + { + addTestInputManagerStep(false); AddStep("press primary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Primary)); AddStep("press auxiliary button 4", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); @@ -233,12 +325,8 @@ public void TestTabletButtonInput() AddStep("press auxiliary button 2", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button2)); AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("pen buttons synced properly", () => - !testInputManager.CurrentState.Tablet.PenButtons.Contains(TabletPenButton.Primary) - && testInputManager.CurrentState.Tablet.PenButtons.Contains(TabletPenButton.Tertiary)); - AddAssert("auxiliary buttons synced properly", () => - !testInputManager.CurrentState.Tablet.AuxiliaryButtons.Contains(TabletAuxiliaryButton.Button4) - && testInputManager.CurrentState.Tablet.AuxiliaryButtons.Contains(TabletAuxiliaryButton.Button2)); + AddAssert("synced release only", () => !testInputManager.CurrentState.Tablet.PenButtons.HasAnyButtonPressed); + AddAssert("auxiliary buttons synced release only", () => !testInputManager.CurrentState.Tablet.AuxiliaryButtons.HasAnyButtonPressed); } public partial class TestInputManager : ManualInputManager diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index 0ce65c9fb7..40855fa268 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -52,6 +52,9 @@ public virtual bool UseParentInput /// Whether to synchronise newly pressed buttons on . /// Pressed buttons are still synchronised at the point of loading the input manager. /// + /// + /// This does not apply for mouse buttons. + /// public bool SyncNewPresses { get; init; } = true; public override bool HandleHoverEvents => UseParentInput ? parentInputManager.HandleHoverEvents : base.HandleHoverEvents; @@ -250,8 +253,12 @@ protected virtual void SyncInputState(InputState state, bool applyPresses) if (SyncNewPresses || applyPresses) { - if (mouseDiff.Pressed.Length > 0) - new MouseButtonInput(mouseDiff.Pressed.Select(button => new ButtonInputEntry(button, true))).Apply(CurrentState, this); + // since we have a flag for syncing new presses, it is expected that we apply mouse button presses as well. + // this wasn't the case before the existence of the flag for another reason that's irrelevant here, + // but we still cannot apply mouse button presses because it conflicts with the mouse-from-touch logic. + // e.g. if a parent input manager does not have a drawable handling touch and transforms touch1 to left mouse, + // then this input manager shouldn't apply left mouse, as it may have a drawable handling touch. this is covered in tests. + foreach (var key in keyDiff.Pressed) new KeyboardKeyInput(key, true).Apply(CurrentState, this); if (touchDiff.activated.Length > 0) From 144143bae7bd77ab7440158bbe64938f1cff1006 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Mar 2024 12:30:09 +0300 Subject: [PATCH 004/140] Adjust tests with special mouse behaviour --- .../Input/TestScenePassThroughInputManager.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs index 2f189307e0..6b89b8d1a0 100644 --- a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs +++ b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs @@ -57,7 +57,7 @@ public void ReceiveInitialState() AddStep("Press A", () => InputManager.PressKey(Key.A)); AddStep("Press Joystick", () => InputManager.PressJoystickButton(JoystickButton.Button1)); addTestInputManagerStep(); - AddAssert("mouse left pressed", () => mouse.IsPressed(MouseButton.Left)); + AddAssert("mouse left not pressed", () => !mouse.IsPressed(MouseButton.Left)); AddAssert("A pressed", () => keyboard.IsPressed(Key.A)); AddAssert("Joystick pressed", () => joystick.IsPressed(JoystickButton.Button1)); AddStep("Release", () => @@ -109,13 +109,13 @@ public void TestUpReceivedOnDownFromSync() } [Test] - public void TestMouseInput() + public void TestMouseDownNoSync([Values] bool syncNewPresses) { - addTestInputManagerStep(); + addTestInputManagerStep(syncNewPresses); AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); AddStep("Press left", () => InputManager.PressButton(MouseButton.Left)); AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("pressed", () => mouse.IsPressed(MouseButton.Left)); + AddAssert("not pressed", () => !mouse.IsPressed(MouseButton.Left)); } [Test] @@ -261,16 +261,6 @@ public void TestKeyInput_DisabledSyncNewPresses() AddAssert("mouse still not pressed", () => !testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed); } - [Test] - public void TestMouseInput_DisabledSyncNewPresses() - { - addTestInputManagerStep(false); - AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); - AddStep("Press left", () => InputManager.PressButton(MouseButton.Left)); - AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("not pressed", () => !mouse.IsPressed(MouseButton.Left)); - } - [Test] public void TestTouchInput_DisabledSyncNewPresses() { From 401de6cae617bf48abfa0c0b8189591f2e42df96 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 22 Mar 2024 07:13:32 +0300 Subject: [PATCH 005/140] Refactor `PassThroughInputManager` once more for readability --- .../Input/PassThroughInputManager.cs | 174 +++++++++--------- 1 file changed, 84 insertions(+), 90 deletions(-) diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index 40855fa268..69a686308b 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; @@ -29,6 +27,8 @@ namespace osu.Framework.Input /// public partial class PassThroughInputManager : CustomInputManager, IRequireHighFrequencyMousePosition { + private bool useParentInput = true; + /// /// If there's an InputManager above us, decide whether we should use their available state. /// @@ -42,22 +42,32 @@ public virtual bool UseParentInput useParentInput = value; if (UseParentInput) - Sync(); + syncReleasedButtons(); } } - private bool useParentInput = true; + private InputManager? parentInputManager; - /// - /// Whether to synchronise newly pressed buttons on . - /// Pressed buttons are still synchronised at the point of loading the input manager. - /// - /// - /// This does not apply for mouse buttons. - /// - public bool SyncNewPresses { get; init; } = true; + protected override void LoadComplete() + { + base.LoadComplete(); + + parentInputManager = GetContainingInputManager(); + applyInitialState(); + } + + protected override void Update() + { + base.Update(); + + // this is usually not needed because we're guaranteed to receive release events as long as we have received press events beforehand. + // however, we sync our state with parent input manager on load, and we cannot receive release events for those inputs, + // therefore we're forced to check every update frame. + if (UseParentInput) + syncReleasedButtons(); + } - public override bool HandleHoverEvents => UseParentInput ? parentInputManager.HandleHoverEvents : base.HandleHoverEvents; + public override bool HandleHoverEvents => parentInputManager != null && UseParentInput ? parentInputManager.HandleHoverEvents : base.HandleHoverEvents; internal override bool BuildNonPositionalInputQueue(List queue, bool allowBlocking = true) { @@ -85,9 +95,7 @@ protected override List GetPendingInputs() var pendingInputs = base.GetPendingInputs(); if (UseParentInput) - { pendingInputs.Clear(); - } return pendingInputs; } @@ -104,15 +112,11 @@ protected override bool Handle(UIEvent e) switch (e) { case MouseDownEvent mouseDown: - // safe-guard for edge cases. - if (!CurrentState.Mouse.IsPressed(mouseDown.Button)) - new MouseButtonInput(mouseDown.Button, true).Apply(CurrentState, this); + new MouseButtonInput(mouseDown.Button, true).Apply(CurrentState, this); break; case MouseUpEvent mouseUp: - // safe-guard for edge cases. - if (CurrentState.Mouse.IsPressed(mouseUp.Button)) - new MouseButtonInput(mouseUp.Button, false).Apply(CurrentState, this); + new MouseButtonInput(mouseUp.Button, false).Apply(CurrentState, this); break; case MouseMoveEvent mouseMove: @@ -125,9 +129,10 @@ protected override bool Handle(UIEvent e) break; case KeyDownEvent keyDown: - if (!keyDown.Repeat) - new KeyboardKeyInput(keyDown.Key, true).Apply(CurrentState, this); + if (keyDown.Repeat) + return false; + new KeyboardKeyInput(keyDown.Key, true).Apply(CurrentState, this); break; case KeyUpEvent keyUp: @@ -178,52 +183,53 @@ protected override bool Handle(UIEvent e) return false; } - private InputManager parentInputManager; - - protected override void LoadComplete() + private void applyInitialState() { - base.LoadComplete(); - Sync(applyPresses: true); - } - - protected override void Update() - { - base.Update(); - - // Some non-positional events are blocked. Sync every frame. - if (UseParentInput) Sync(true); - } - - /// - /// Sync input state to parent 's . - /// Call this when parent changed somehow. - /// - /// If this is false, assume parent input manager is unchanged from before. - /// Whether to also apply press inputs for buttons that are shown to be pressed in the parent input manager. - public void Sync(bool useCachedParentInputManager = false, bool applyPresses = false) - { - if (!UseParentInput) return; - - if (!useCachedParentInputManager) - parentInputManager = GetContainingInputManager(); - - SyncInputState(parentInputManager?.CurrentState, applyPresses); + if (parentInputManager == null) + return; + + var parentState = parentInputManager.CurrentState; + var mouseDiff = (parentState?.Mouse?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Mouse.Buttons); + var keyDiff = (parentState?.Keyboard.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Keyboard.Keys); + var touchDiff = (parentState?.Touch ?? new TouchState()).EnumerateDifference(CurrentState.Touch); + var joyButtonDiff = (parentState?.Joystick?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Joystick.Buttons); + var midiDiff = (parentState?.Midi?.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Midi.Keys); + var tabletPenDiff = (parentState?.Tablet?.PenButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.PenButtons); + var tabletAuxiliaryDiff = (parentState?.Tablet?.AuxiliaryButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons); + + // we should not read mouse button presses from parent as the source of those presses may originate from touch. + // e.g. if a parent input manager does not have a drawable handling touch and transforms touch1 to left mouse, + // then this input manager shouldn't apply left mouse, as it may have a drawable handling touch. this is covered in tests. + + foreach (var key in keyDiff.Pressed) + new KeyboardKeyInput(key, true).Apply(CurrentState, this); + if (touchDiff.deactivated.Length > 0) + new TouchInput(touchDiff.deactivated, true).Apply(CurrentState, this); + foreach (var button in joyButtonDiff.Pressed) + new JoystickButtonInput(button, true).Apply(CurrentState, this); + foreach (var key in midiDiff.Pressed) + new MidiKeyInput(key, parentState?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, true).Apply(CurrentState, this); + foreach (var button in tabletPenDiff.Pressed) + new TabletPenButtonInput(button, true).Apply(CurrentState, this); + foreach (var button in tabletAuxiliaryDiff.Pressed) + new TabletAuxiliaryButtonInput(button, true).Apply(CurrentState, this); + + syncJoystickAxes(); } - /// - /// Sync current state to a certain state. - /// - /// The state to synchronise current with. If this is null, it is regarded as an empty state. - /// Whether to also apply press inputs for buttons that are shown to be pressed in the parent input manager. - protected virtual void SyncInputState(InputState state, bool applyPresses) + private void syncReleasedButtons() { - var mouseDiff = (state?.Mouse?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Mouse.Buttons); - var keyDiff = (state?.Keyboard.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Keyboard.Keys); - var touchDiff = (state?.Touch ?? new TouchState()).EnumerateDifference(CurrentState.Touch); - var joyButtonDiff = (state?.Joystick?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Joystick.Buttons); - var midiDiff = (state?.Midi?.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Midi.Keys); - var tabletPenDiff = (state?.Tablet?.PenButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.PenButtons); - var tabletAuxiliaryDiff = (state?.Tablet?.AuxiliaryButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons); + if (parentInputManager == null) + return; + + var parentState = parentInputManager.CurrentState; + var mouseDiff = (parentState?.Mouse?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Mouse.Buttons); + var keyDiff = (parentState?.Keyboard.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Keyboard.Keys); + var touchDiff = (parentState?.Touch ?? new TouchState()).EnumerateDifference(CurrentState.Touch); + var joyButtonDiff = (parentState?.Joystick?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Joystick.Buttons); + var midiDiff = (parentState?.Midi?.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Midi.Keys); + var tabletPenDiff = (parentState?.Tablet?.PenButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.PenButtons); + var tabletAuxiliaryDiff = (parentState?.Tablet?.AuxiliaryButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons); if (mouseDiff.Released.Length > 0) new MouseButtonInput(mouseDiff.Released.Select(button => new ButtonInputEntry(button, false))).Apply(CurrentState, this); @@ -234,44 +240,32 @@ protected virtual void SyncInputState(InputState state, bool applyPresses) foreach (var button in joyButtonDiff.Released) new JoystickButtonInput(button, false).Apply(CurrentState, this); foreach (var key in midiDiff.Released) - new MidiKeyInput(key, state?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, false).Apply(CurrentState, this); + new MidiKeyInput(key, parentState?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, false).Apply(CurrentState, this); foreach (var button in tabletPenDiff.Released) new TabletPenButtonInput(button, false).Apply(CurrentState, this); foreach (var button in tabletAuxiliaryDiff.Released) new TabletAuxiliaryButtonInput(button, false).Apply(CurrentState, this); + syncJoystickAxes(); + } + + private void syncJoystickAxes() + { + if (parentInputManager == null) + return; + + var parentState = parentInputManager.CurrentState; + // Basically only perform the full state diff if we have found that any axis changed. // This avoids unnecessary alloc overhead. for (int i = 0; i < JoystickState.MAX_AXES; i++) { - if (state?.Joystick?.AxesValues[i] != CurrentState.Joystick.AxesValues[i]) + if (parentState?.Joystick?.AxesValues[i] != CurrentState.Joystick.AxesValues[i]) { - new JoystickAxisInput(state?.Joystick?.GetAxes() ?? Array.Empty()).Apply(CurrentState, this); + new JoystickAxisInput(parentState?.Joystick?.GetAxes() ?? Array.Empty()).Apply(CurrentState, this); break; } } - - if (SyncNewPresses || applyPresses) - { - // since we have a flag for syncing new presses, it is expected that we apply mouse button presses as well. - // this wasn't the case before the existence of the flag for another reason that's irrelevant here, - // but we still cannot apply mouse button presses because it conflicts with the mouse-from-touch logic. - // e.g. if a parent input manager does not have a drawable handling touch and transforms touch1 to left mouse, - // then this input manager shouldn't apply left mouse, as it may have a drawable handling touch. this is covered in tests. - - foreach (var key in keyDiff.Pressed) - new KeyboardKeyInput(key, true).Apply(CurrentState, this); - if (touchDiff.activated.Length > 0) - new TouchInput(touchDiff.activated, true).Apply(CurrentState, this); - foreach (var button in joyButtonDiff.Pressed) - new JoystickButtonInput(button, true).Apply(CurrentState, this); - foreach (var key in midiDiff.Pressed) - new MidiKeyInput(key, state?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, true).Apply(CurrentState, this); - foreach (var button in tabletPenDiff.Pressed) - new TabletPenButtonInput(button, true).Apply(CurrentState, this); - foreach (var button in tabletAuxiliaryDiff.Pressed) - new TabletAuxiliaryButtonInput(button, true).Apply(CurrentState, this); - } } } } From a072917dc90caafd8039e51668dfd3fd5c330631 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 22 Mar 2024 07:13:40 +0300 Subject: [PATCH 006/140] Update test coverage --- .../Visual/Input/TestSceneInputManager.cs | 12 +- .../Input/TestScenePassThroughInputManager.cs | 178 ++++++------------ 2 files changed, 69 insertions(+), 121 deletions(-) diff --git a/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs b/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs index 42ff0cd1b5..8fc1ffe1d1 100644 --- a/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs +++ b/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs @@ -74,6 +74,9 @@ public partial class ContainingInputManagerStatusText : Container mouseStatus, keyboardStatus, joystickStatus, + touchStatus, + midiStatus, + tabletStatus, onMouseDownStatus, onMouseUpStatus, onMouseMoveStatus, @@ -100,6 +103,9 @@ public ContainingInputManagerStatusText() mouseStatus = new SmallText(), keyboardStatus = new SmallText(), joystickStatus = new SmallText(), + touchStatus = new SmallText(), + midiStatus = new SmallText(), + tabletStatus = new SmallText(), onMouseDownStatus = new SmallText { Text = "OnMouseDown 0" }, onMouseUpStatus = new SmallText { Text = "OnMouseUp 0" }, onMouseMoveStatus = new SmallText { Text = "OnMouseMove 0" }, @@ -114,11 +120,13 @@ protected override void Update() { var inputManager = GetContainingInputManager(); var currentState = inputManager.CurrentState; - var mouse = currentState.Mouse; inputManagerStatus.Text = $"{inputManager}"; - mouseStatus.Text = $"Mouse: {mouse.Position} {mouse.Scroll} " + string.Join(' ', mouse.Buttons); + mouseStatus.Text = $"Mouse: {currentState.Mouse.Position} {currentState.Mouse.Scroll} " + string.Join(' ', currentState.Mouse.Buttons); keyboardStatus.Text = "Keyboard: " + string.Join(' ', currentState.Keyboard.Keys); joystickStatus.Text = "Joystick: " + string.Join(' ', currentState.Joystick.Buttons); + touchStatus.Text = $"Touch: {string.Join(' ', currentState.Touch.ActiveSources.Select(s => $"({s},{currentState.Touch.GetTouchPosition(s)})"))}"; + midiStatus.Text = "MIDI: " + string.Join(' ', currentState.Midi.Keys.Select(k => $"({k},{currentState.Midi.Velocities[k]})")); + tabletStatus.Text = "Tablet: " + string.Join(' ', currentState.Tablet.PenButtons) + " " + string.Join(' ', currentState.Tablet.AuxiliaryButtons); base.Update(); } diff --git a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs index 6b89b8d1a0..9e7d852f9e 100644 --- a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs +++ b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs @@ -31,11 +31,11 @@ public TestScenePassThroughInputManager() private ButtonStates keyboard; private ButtonStates joystick; - private void addTestInputManagerStep(bool syncNewPresses = true) + private void addTestInputManagerStep() { AddStep("Add InputManager", () => { - testInputManager = new TestInputManager { SyncNewPresses = syncNewPresses }; + testInputManager = new TestInputManager(); Add(testInputManager); state = testInputManager.CurrentState; mouse = state.Mouse.Buttons; @@ -94,25 +94,10 @@ public void UseParentInputChange() } [Test] - public void TestUpReceivedOnDownFromSync() + public void TestMouseDownNoSync() { addTestInputManagerStep(); AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); - AddStep("press keyboard", () => InputManager.PressKey(Key.A)); - AddAssert("key not pressed", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); - - AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("key pressed", () => testInputManager.CurrentState.Keyboard.Keys.Single() == Key.A); - - AddStep("release keyboard", () => InputManager.ReleaseKey(Key.A)); - AddAssert("key released", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); - } - - [Test] - public void TestMouseDownNoSync([Values] bool syncNewPresses) - { - addTestInputManagerStep(syncNewPresses); - AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); AddStep("Press left", () => InputManager.PressButton(MouseButton.Left)); AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); AddAssert("not pressed", () => !mouse.IsPressed(MouseButton.Left)); @@ -134,28 +119,50 @@ public void TestNoMouseUp() AddAssert("mouse up count == 0", () => testInputManager.Status.MouseUpCount == 0); } + [Test] + public void TestKeyInput() + { + addTestInputManagerStep(); + AddStep("press key", () => InputManager.PressKey(Key.A)); + AddAssert("key pressed", () => testInputManager.CurrentState.Keyboard.Keys.Single() == Key.A); + + AddStep("release key", () => InputManager.ReleaseKey(Key.A)); + AddAssert("key released", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); + + AddStep("press key", () => InputManager.PressKey(Key.A)); + AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); + AddStep("release key", () => InputManager.ReleaseKey(Key.A)); + AddStep("press another key", () => InputManager.PressKey(Key.B)); + + AddAssert("only first key pressed", () => testInputManager.CurrentState.Keyboard.Keys.Single() == Key.A); + + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); + AddAssert("key released", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); + } + [Test] public void TestTouchInput() { addTestInputManagerStep(); AddStep("begin first touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); - AddAssert("synced properly", () => + AddAssert("first touch active", () => testInputManager.CurrentState.Touch.ActiveSources.Single() == TouchSource.Touch1 && testInputManager.CurrentState.Touch.TouchPositions[(int)TouchSource.Touch1] == Vector2.Zero); + AddStep("end first touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); + AddAssert("first touch not active", () => !testInputManager.CurrentState.Touch.ActiveSources.HasAnyButtonPressed); + + AddStep("begin first touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); AddStep("end first touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); AddStep("begin second touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, Vector2.One))); + AddAssert("only first touch active", () => + testInputManager.CurrentState.Touch.ActiveSources.Single() == TouchSource.Touch1 && + testInputManager.CurrentState.Touch.TouchPositions[(int)TouchSource.Touch1] == Vector2.Zero); + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("synced properly", () => - testInputManager.CurrentState.Touch.ActiveSources.Single() == TouchSource.Touch2 && - testInputManager.CurrentState.Touch.TouchPositions[(int)TouchSource.Touch2] == Vector2.One); - - AddStep("end second touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, new Vector2(2)))); - AddAssert("synced properly", () => - !testInputManager.CurrentState.Touch.ActiveSources.HasAnyButtonPressed && - testInputManager.CurrentState.Touch.TouchPositions[(int)TouchSource.Touch2] == new Vector2(2)); + AddAssert("no touches active", () => !testInputManager.CurrentState.Touch.ActiveSources.HasAnyButtonPressed); } [Test] @@ -164,20 +171,24 @@ public void TestMidiInput() addTestInputManagerStep(); AddStep("press C3", () => InputManager.PressMidiKey(MidiKey.C3, 70)); - AddAssert("synced properly", () => + AddAssert("C3 pressed", () => testInputManager.CurrentState.Midi.Keys.IsPressed(MidiKey.C3) && testInputManager.CurrentState.Midi.Velocities[MidiKey.C3] == 70); + AddStep("release C3", () => InputManager.ReleaseMidiKey(MidiKey.C3, 40)); + AddAssert("C3 released", () => !testInputManager.CurrentState.Midi.Keys.HasAnyButtonPressed); + + AddStep("press C3", () => InputManager.PressMidiKey(MidiKey.C3, 70)); AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); AddStep("release C3", () => InputManager.ReleaseMidiKey(MidiKey.C3, 40)); AddStep("press F#3", () => InputManager.PressMidiKey(MidiKey.FSharp3, 65)); + AddAssert("only C3 pressed", () => + testInputManager.CurrentState.Midi.Keys.Single() == MidiKey.C3 + && testInputManager.CurrentState.Midi.Velocities[MidiKey.C3] == 70); + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("synced properly", () => - !testInputManager.CurrentState.Midi.Keys.IsPressed(MidiKey.C3) && - testInputManager.CurrentState.Midi.Velocities[MidiKey.C3] == 40 && - testInputManager.CurrentState.Midi.Keys.IsPressed(MidiKey.FSharp3) && - testInputManager.CurrentState.Midi.Velocities[MidiKey.FSharp3] == 65); + AddAssert("C3 released", () => !testInputManager.CurrentState.Midi.Keys.HasAnyButtonPressed); } [Test] @@ -187,21 +198,28 @@ public void TestTabletButtonInput() AddStep("press primary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Primary)); AddStep("press auxiliary button 4", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); + AddAssert("primary pen button pressed", () => testInputManager.CurrentState.Tablet.PenButtons.Single() == TabletPenButton.Primary); + AddAssert("auxiliary button 4 pressed", () => testInputManager.CurrentState.Tablet.AuxiliaryButtons.Single() == TabletAuxiliaryButton.Button4); - AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); + AddStep("release primary pen button", () => InputManager.ReleaseTabletPenButton(TabletPenButton.Primary)); + AddStep("release auxiliary button 4", () => InputManager.ReleaseTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); + AddAssert("primary pen button pressed", () => !testInputManager.CurrentState.Tablet.PenButtons.HasAnyButtonPressed); + AddAssert("auxiliary button 4 pressed", () => !testInputManager.CurrentState.Tablet.AuxiliaryButtons.HasAnyButtonPressed); + AddStep("press primary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Primary)); + AddStep("press auxiliary button 4", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); + AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); AddStep("release primary pen button", () => InputManager.ReleaseTabletPenButton(TabletPenButton.Primary)); - AddStep("press tertiary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Tertiary)); AddStep("release auxiliary button 4", () => InputManager.ReleaseTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); + AddStep("press secondary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Secondary)); AddStep("press auxiliary button 2", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button2)); + AddAssert("only primary pen button pressed", () => testInputManager.CurrentState.Tablet.PenButtons.Single() == TabletPenButton.Primary); + AddAssert("only auxiliary button 4 pressed", () => testInputManager.CurrentState.Tablet.AuxiliaryButtons.Single() == TabletAuxiliaryButton.Button4); + AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("pen buttons synced properly", () => - !testInputManager.CurrentState.Tablet.PenButtons.Contains(TabletPenButton.Primary) - && testInputManager.CurrentState.Tablet.PenButtons.Contains(TabletPenButton.Tertiary)); - AddAssert("auxiliary buttons synced properly", () => - !testInputManager.CurrentState.Tablet.AuxiliaryButtons.Contains(TabletAuxiliaryButton.Button4) - && testInputManager.CurrentState.Tablet.AuxiliaryButtons.Contains(TabletAuxiliaryButton.Button2)); + AddAssert("primary pen button released", () => !testInputManager.CurrentState.Tablet.PenButtons.HasAnyButtonPressed); + AddAssert("auxiliary button 4 released", () => !testInputManager.CurrentState.Tablet.AuxiliaryButtons.HasAnyButtonPressed); } [Test] @@ -241,84 +259,6 @@ public void TestMouseTouchProductionOnPassThrough() AddAssert("pass-through handled mouse", () => testInputManager.CurrentState.Mouse.Buttons.Single() == MouseButton.Left); } - [Test] - public void TestKeyInput_DisabledSyncNewPresses() - { - addTestInputManagerStep(false); - AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); - AddStep("press keyboard", () => InputManager.PressKey(Key.A)); - AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); - AddAssert("key not pressed", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); - AddAssert("mouse not pressed", () => !testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed); - - AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("key still not pressed", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); - AddAssert("mouse still not pressed", () => !testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed); - - AddStep("release keyboard", () => InputManager.ReleaseKey(Key.A)); - AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); - AddAssert("key still not pressed", () => !testInputManager.CurrentState.Keyboard.Keys.HasAnyButtonPressed); - AddAssert("mouse still not pressed", () => !testInputManager.CurrentState.Mouse.Buttons.HasAnyButtonPressed); - } - - [Test] - public void TestTouchInput_DisabledSyncNewPresses() - { - addTestInputManagerStep(false); - AddStep("begin first touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); - AddAssert("synced properly", () => - testInputManager.CurrentState.Touch.ActiveSources.Single() == TouchSource.Touch1 && - testInputManager.CurrentState.Touch.TouchPositions[(int)TouchSource.Touch1] == Vector2.Zero); - - AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); - AddStep("end first touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, Vector2.Zero))); - AddStep("begin second touch", () => InputManager.BeginTouch(new Touch(TouchSource.Touch2, Vector2.One))); - - AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("synced release only", () => !testInputManager.CurrentState.Touch.ActiveSources.HasAnyButtonPressed); - - AddStep("end second touch", () => InputManager.EndTouch(new Touch(TouchSource.Touch2, new Vector2(2)))); - AddAssert("synced release only", () => !testInputManager.CurrentState.Touch.ActiveSources.HasAnyButtonPressed); - } - - [Test] - public void TestMidiInput_DisabledSyncNewPresses() - { - addTestInputManagerStep(false); - - AddStep("press C3", () => InputManager.PressMidiKey(MidiKey.C3, 70)); - AddAssert("synced properly", () => - testInputManager.CurrentState.Midi.Keys.IsPressed(MidiKey.C3) - && testInputManager.CurrentState.Midi.Velocities[MidiKey.C3] == 70); - - AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); - AddStep("release C3", () => InputManager.ReleaseMidiKey(MidiKey.C3, 40)); - AddStep("press F#3", () => InputManager.PressMidiKey(MidiKey.FSharp3, 65)); - - AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("synced release only", () => !testInputManager.CurrentState.Midi.Keys.HasAnyButtonPressed); - } - - [Test] - public void TestTabletButtonInput_DisabledSyncNewPresses() - { - addTestInputManagerStep(false); - - AddStep("press primary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Primary)); - AddStep("press auxiliary button 4", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); - - AddStep("UseParentInput = false", () => testInputManager.UseParentInput = false); - - AddStep("release primary pen button", () => InputManager.ReleaseTabletPenButton(TabletPenButton.Primary)); - AddStep("press tertiary pen button", () => InputManager.PressTabletPenButton(TabletPenButton.Tertiary)); - AddStep("release auxiliary button 4", () => InputManager.ReleaseTabletAuxiliaryButton(TabletAuxiliaryButton.Button4)); - AddStep("press auxiliary button 2", () => InputManager.PressTabletAuxiliaryButton(TabletAuxiliaryButton.Button2)); - - AddStep("UseParentInput = true", () => testInputManager.UseParentInput = true); - AddAssert("synced release only", () => !testInputManager.CurrentState.Tablet.PenButtons.HasAnyButtonPressed); - AddAssert("auxiliary buttons synced release only", () => !testInputManager.CurrentState.Tablet.AuxiliaryButtons.HasAnyButtonPressed); - } - public partial class TestInputManager : ManualInputManager { public readonly TestSceneInputManager.ContainingInputManagerStatusText Status; From 232559ed10af2d3320400fa849927c2b6ff063bf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 22 Mar 2024 08:09:36 +0300 Subject: [PATCH 007/140] Remove initial state synchronisation --- .../Input/TestScenePassThroughInputManager.cs | 19 ------ .../Input/PassThroughInputManager.cs | 65 +++---------------- 2 files changed, 8 insertions(+), 76 deletions(-) diff --git a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs index 9e7d852f9e..3b1e485bc0 100644 --- a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs +++ b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs @@ -50,25 +50,6 @@ private void addTestInputManagerStep() ChildrenEnumerable = Enumerable.Empty(); }); - [Test] - public void ReceiveInitialState() - { - AddStep("Press mouse left", () => InputManager.PressButton(MouseButton.Left)); - AddStep("Press A", () => InputManager.PressKey(Key.A)); - AddStep("Press Joystick", () => InputManager.PressJoystickButton(JoystickButton.Button1)); - addTestInputManagerStep(); - AddAssert("mouse left not pressed", () => !mouse.IsPressed(MouseButton.Left)); - AddAssert("A pressed", () => keyboard.IsPressed(Key.A)); - AddAssert("Joystick pressed", () => joystick.IsPressed(JoystickButton.Button1)); - AddStep("Release", () => - { - InputManager.ReleaseButton(MouseButton.Left); - InputManager.ReleaseKey(Key.A); - InputManager.ReleaseJoystickButton(JoystickButton.Button1); - }); - AddAssert("All released", () => !mouse.HasAnyButtonPressed && !keyboard.HasAnyButtonPressed && !joystick.HasAnyButtonPressed); - } - [Test] public void UseParentInputChange() { diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index 69a686308b..7c049b6a2f 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -42,7 +42,7 @@ public virtual bool UseParentInput useParentInput = value; if (UseParentInput) - syncReleasedButtons(); + syncIgnoredInput(); } } @@ -51,20 +51,7 @@ public virtual bool UseParentInput protected override void LoadComplete() { base.LoadComplete(); - parentInputManager = GetContainingInputManager(); - applyInitialState(); - } - - protected override void Update() - { - base.Update(); - - // this is usually not needed because we're guaranteed to receive release events as long as we have received press events beforehand. - // however, we sync our state with parent input manager on load, and we cannot receive release events for those inputs, - // therefore we're forced to check every update frame. - if (UseParentInput) - syncReleasedButtons(); } public override bool HandleHoverEvents => parentInputManager != null && UseParentInput ? parentInputManager.HandleHoverEvents : base.HandleHoverEvents; @@ -183,41 +170,10 @@ protected override bool Handle(UIEvent e) return false; } - private void applyInitialState() - { - if (parentInputManager == null) - return; - - var parentState = parentInputManager.CurrentState; - var mouseDiff = (parentState?.Mouse?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Mouse.Buttons); - var keyDiff = (parentState?.Keyboard.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Keyboard.Keys); - var touchDiff = (parentState?.Touch ?? new TouchState()).EnumerateDifference(CurrentState.Touch); - var joyButtonDiff = (parentState?.Joystick?.Buttons ?? new ButtonStates()).EnumerateDifference(CurrentState.Joystick.Buttons); - var midiDiff = (parentState?.Midi?.Keys ?? new ButtonStates()).EnumerateDifference(CurrentState.Midi.Keys); - var tabletPenDiff = (parentState?.Tablet?.PenButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.PenButtons); - var tabletAuxiliaryDiff = (parentState?.Tablet?.AuxiliaryButtons ?? new ButtonStates()).EnumerateDifference(CurrentState.Tablet.AuxiliaryButtons); - - // we should not read mouse button presses from parent as the source of those presses may originate from touch. - // e.g. if a parent input manager does not have a drawable handling touch and transforms touch1 to left mouse, - // then this input manager shouldn't apply left mouse, as it may have a drawable handling touch. this is covered in tests. - - foreach (var key in keyDiff.Pressed) - new KeyboardKeyInput(key, true).Apply(CurrentState, this); - if (touchDiff.deactivated.Length > 0) - new TouchInput(touchDiff.deactivated, true).Apply(CurrentState, this); - foreach (var button in joyButtonDiff.Pressed) - new JoystickButtonInput(button, true).Apply(CurrentState, this); - foreach (var key in midiDiff.Pressed) - new MidiKeyInput(key, parentState?.Midi?.Velocities.GetValueOrDefault(key) ?? 0, true).Apply(CurrentState, this); - foreach (var button in tabletPenDiff.Pressed) - new TabletPenButtonInput(button, true).Apply(CurrentState, this); - foreach (var button in tabletAuxiliaryDiff.Pressed) - new TabletAuxiliaryButtonInput(button, true).Apply(CurrentState, this); - - syncJoystickAxes(); - } - - private void syncReleasedButtons() + /// + /// Updates state of any buttons that have been released by parent or axes that have changed value while was disabled. + /// + private void syncIgnoredInput() { if (parentInputManager == null) return; @@ -246,23 +202,18 @@ private void syncReleasedButtons() foreach (var button in tabletAuxiliaryDiff.Released) new TabletAuxiliaryButtonInput(button, false).Apply(CurrentState, this); - syncJoystickAxes(); - } - - private void syncJoystickAxes() - { if (parentInputManager == null) return; - var parentState = parentInputManager.CurrentState; + var parentState1 = parentInputManager.CurrentState; // Basically only perform the full state diff if we have found that any axis changed. // This avoids unnecessary alloc overhead. for (int i = 0; i < JoystickState.MAX_AXES; i++) { - if (parentState?.Joystick?.AxesValues[i] != CurrentState.Joystick.AxesValues[i]) + if (parentState1?.Joystick?.AxesValues[i] != CurrentState.Joystick.AxesValues[i]) { - new JoystickAxisInput(parentState?.Joystick?.GetAxes() ?? Array.Empty()).Apply(CurrentState, this); + new JoystickAxisInput(parentState1?.Joystick?.GetAxes() ?? Array.Empty()).Apply(CurrentState, this); break; } } From 8717836e12348fe3500087bdcf375151e7090d69 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 24 Mar 2024 07:09:08 +0300 Subject: [PATCH 008/140] Fix copy pasta --- osu.Framework/Input/PassThroughInputManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index 7c049b6a2f..8e830e38ac 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -162,8 +162,8 @@ protected override bool Handle(UIEvent e) new TabletAuxiliaryButtonInput(tabletAuxiliaryButtonPress.Button, true).Apply(CurrentState, this); break; - case TabletAuxiliaryButtonReleaseEvent tabletAuxiliaryButtonPress: - new TabletAuxiliaryButtonInput(tabletAuxiliaryButtonPress.Button, true).Apply(CurrentState, this); + case TabletAuxiliaryButtonReleaseEvent tabletAuxiliaryButtonRelease: + new TabletAuxiliaryButtonInput(tabletAuxiliaryButtonRelease.Button, false).Apply(CurrentState, this); break; } From 4c56f6dd190f4c8306d877736ae3bd96e224cc49 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 24 Mar 2024 06:18:17 +0300 Subject: [PATCH 009/140] Add failing test case Ensure releasing key after turning off parent input still behaves correctly --- .../Visual/Input/TestSceneInputManager.cs | 8 ++++ .../Input/TestScenePassThroughInputManager.cs | 47 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs b/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs index 8fc1ffe1d1..ad711024c3 100644 --- a/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs +++ b/osu.Framework.Tests/Visual/Input/TestSceneInputManager.cs @@ -175,6 +175,14 @@ protected override bool OnHover(HoverEvent e) return base.OnHover(e); } + public int KeyDownCount; + + protected override bool OnKeyDown(KeyDownEvent e) + { + ++KeyDownCount; + return base.OnKeyDown(e); + } + protected override bool OnClick(ClickEvent e) { this.MoveToOffset(new Vector2(100, 0)).Then().MoveToOffset(new Vector2(-100, 0), 1000, Easing.In); diff --git a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs index 3b1e485bc0..352a9928d7 100644 --- a/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs +++ b/osu.Framework.Tests/Visual/Input/TestScenePassThroughInputManager.cs @@ -240,6 +240,53 @@ public void TestMouseTouchProductionOnPassThrough() AddAssert("pass-through handled mouse", () => testInputManager.CurrentState.Mouse.Buttons.Single() == MouseButton.Left); } + /// + /// Ensures that does not handle input within the frame that is enabled. + /// + [Test] + public void TestInputPropagation() + { + addTestInputManagerStep(); + AddStep("setup hierarchy", () => + { + Add(new PassThroughInputManager + { + Name = "other input manager", + Child = new HandlingBox + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + OnHandle = e => + { + if (e is KeyDownEvent keyDown && !keyDown.Repeat) + { + testInputManager.UseParentInput = true; + return true; + } + + return false; + } + }, + }); + }); + + AddStep("turn off parent input", () => testInputManager.UseParentInput = false); + AddStep("press key", () => InputManager.PressKey(Key.A)); + AddAssert("key not pressed in pass-through", () => !keyboard.IsPressed(Key.A)); + AddAssert("no key down event inside pass-through", () => testInputManager.Status.KeyDownCount == 0); + + // parent input should be turned on by the box handler above. + AddAssert("parent input turned on", () => testInputManager.UseParentInput); + AddStep("release key", () => InputManager.ReleaseKey(Key.A)); + + AddStep("press key", () => InputManager.PressKey(Key.A)); + AddStep("turn off parent input", () => testInputManager.UseParentInput = false); + AddAssert("key pressed in pass-through", () => keyboard.IsPressed(Key.A)); + + AddStep("release key", () => InputManager.ReleaseKey(Key.A)); + AddAssert("key still pressed in pass-through", () => keyboard.IsPressed(Key.A)); + } + public partial class TestInputManager : ManualInputManager { public readonly TestSceneInputManager.ContainingInputManagerStatusText Status; From 6d6680405b766131d0ea312407778fb0085d9f6f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 24 Mar 2024 06:25:22 +0300 Subject: [PATCH 010/140] Hide `PassThroughInputManager` from input queue if parent input is disabled --- osu.Framework/Input/PassThroughInputManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Framework/Input/PassThroughInputManager.cs b/osu.Framework/Input/PassThroughInputManager.cs index 8e830e38ac..8ecc22c458 100644 --- a/osu.Framework/Input/PassThroughInputManager.cs +++ b/osu.Framework/Input/PassThroughInputManager.cs @@ -54,6 +54,9 @@ protected override void LoadComplete() parentInputManager = GetContainingInputManager(); } + public override bool PropagateNonPositionalInputSubTree => UseParentInput; + public override bool PropagatePositionalInputSubTree => UseParentInput; + public override bool HandleHoverEvents => parentInputManager != null && UseParentInput ? parentInputManager.HandleHoverEvents : base.HandleHoverEvents; internal override bool BuildNonPositionalInputQueue(List queue, bool allowBlocking = true) From a1def8d3fefb56f7993c9926abb758b6862bd08f Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 16:43:01 +0100 Subject: [PATCH 011/140] Fix wrong plurals in param names --- osu.Framework/Input/Bindings/KeyCombination.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 489ca886ab..78c5818ba5 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -106,39 +106,39 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat /// /// Check whether the provided set of pressed keys matches the candidate binding. /// - /// The candidate key binding to match against. - /// The keys which have been pressed by a user. + /// The candidate key binding to match against. + /// The keys which have been pressed by a user. /// The matching mode to be used when checking. /// Whether this is a match. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool ContainsAll(ImmutableArray candidateKey, ImmutableArray pressedKey, KeyCombinationMatchingMode matchingMode) + internal static bool ContainsAll(ImmutableArray candidate, ImmutableArray pressedKeys, KeyCombinationMatchingMode matchingMode) { // first, check that all the candidate keys are contained in the provided pressed keys. // regardless of the matching mode, every key needs to at least be present (matching modes only change // the behaviour of excess keys). - foreach (var key in candidateKey) + foreach (var key in candidate) { - if (!ContainsKey(pressedKey, key)) + if (!ContainsKey(pressedKeys, key)) return false; } switch (matchingMode) { case KeyCombinationMatchingMode.Exact: - foreach (var key in pressedKey) + foreach (var key in pressedKeys) { // in exact matching mode, every pressed key needs to be in the candidate. - if (!ContainsKeyPermissive(candidateKey, key)) + if (!ContainsKeyPermissive(candidate, key)) return false; } break; case KeyCombinationMatchingMode.Modifiers: - foreach (var key in pressedKey) + foreach (var key in pressedKeys) { // in modifiers match mode, the same check applies as exact but only for modifier keys. - if (IsModifierKey(key) && !ContainsKeyPermissive(candidateKey, key)) + if (IsModifierKey(key) && !ContainsKeyPermissive(candidate, key)) return false; } From e05083a750ce673b13d358db98edc17053495a31 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 17:05:32 +0100 Subject: [PATCH 012/140] Don't use `KeyCombination.ContainsKey` for two different purposes --- osu.Framework/Input/Bindings/KeyCombination.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 78c5818ba5..187e594480 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -192,7 +192,7 @@ internal static bool ContainsKeyPermissive(ImmutableArray candidate, I break; } - return ContainsKey(candidate, key); + return candidate.Contains(key); } /// From 3e021ddd197138a97504d59d00fb217aa7868510 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 17:08:08 +0100 Subject: [PATCH 013/140] Remove invalid failing tests These are invalid because they have the virtual `InputKey.Shift` as a pressed key, which never happens in real usage. This is because `KeyCombination.FromKey` will never return a virtual key like `Shift` or `Control`. --- osu.Framework.Tests/Input/KeyCombinationTest.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/osu.Framework.Tests/Input/KeyCombinationTest.cs b/osu.Framework.Tests/Input/KeyCombinationTest.cs index 618c260ce3..d495dee1d5 100644 --- a/osu.Framework.Tests/Input/KeyCombinationTest.cs +++ b/osu.Framework.Tests/Input/KeyCombinationTest.cs @@ -29,22 +29,6 @@ public class KeyCombinationTest new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Modifiers, true }, new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false }, new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, - - // test multiple combination matches. - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Any, true }, - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true }, - new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, false }, - new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Any, true }, - - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Exact, true }, - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, - new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, false }, - new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, - - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true }, - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, - new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false }, - new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.Shift, InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, }; [TestCaseSource(nameof(key_combination_display_test_cases))] From e2316289efcba705a5a48390d3fc6c32495861a6 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 17:14:58 +0100 Subject: [PATCH 014/140] Fix `KeyCombination.ContainsKey` xmldoc and param names --- .../Input/Bindings/KeyCombination.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 187e594480..0722e559ee 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -197,41 +197,41 @@ internal static bool ContainsKeyPermissive(ImmutableArray candidate, I /// /// Check whether a single key from a candidate binding is relevant to the currently pressed keys. - /// If the contains a left/right specific modifier, the must also for this to match. + /// If the contain a left/right specific modifier, the needs to be either that specific modifier or its generic variant to match. /// - /// The candidate key binding to match against. - /// The key which has been pressed by a user. + /// The currently pressed keys to match against. + /// The candidate key to check. /// Whether this is a match. - internal static bool ContainsKey(ImmutableArray candidate, InputKey key) + internal static bool ContainsKey(ImmutableArray pressedKeys, InputKey candidateKey) { - switch (key) + switch (candidateKey) { case InputKey.Control: - if (candidate.Contains(InputKey.LControl) || candidate.Contains(InputKey.RControl)) + if (pressedKeys.Contains(InputKey.LControl) || pressedKeys.Contains(InputKey.RControl)) return true; break; case InputKey.Shift: - if (candidate.Contains(InputKey.LShift) || candidate.Contains(InputKey.RShift)) + if (pressedKeys.Contains(InputKey.LShift) || pressedKeys.Contains(InputKey.RShift)) return true; break; case InputKey.Alt: - if (candidate.Contains(InputKey.LAlt) || candidate.Contains(InputKey.RAlt)) + if (pressedKeys.Contains(InputKey.LAlt) || pressedKeys.Contains(InputKey.RAlt)) return true; break; case InputKey.Super: - if (candidate.Contains(InputKey.LSuper) || candidate.Contains(InputKey.RSuper)) + if (pressedKeys.Contains(InputKey.LSuper) || pressedKeys.Contains(InputKey.RSuper)) return true; break; } - return candidate.Contains(key); + return pressedKeys.Contains(candidateKey); } public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys); From 0ab0adc4c7e8206dc41e6e145feaf86c0e0594ff Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 17:42:16 +0100 Subject: [PATCH 015/140] Simplify `ContainsKeyPermissive` and `ContainsKey` by introducing the concept of virtual keys Where and how exactly `getVirtualKey()` works is subject to change. This is just a MVP. --- .../Input/Bindings/KeyCombination.cs | 105 ++++++------------ 1 file changed, 37 insertions(+), 68 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 0722e559ee..3d1251b88f 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -103,6 +103,30 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat return ContainsAll(Keys, pressedKeys.Keys, matchingMode); } + private static InputKey? getVirtualKey(InputKey key) + { + switch (key) + { + case InputKey.LShift: + case InputKey.RShift: + return InputKey.Shift; + + case InputKey.LControl: + case InputKey.RControl: + return InputKey.Control; + + case InputKey.LAlt: + case InputKey.RAlt: + return InputKey.Alt; + + case InputKey.LSuper: + case InputKey.RSuper: + return InputKey.Super; + } + + return null; + } + /// /// Check whether the provided set of pressed keys matches the candidate binding. /// @@ -113,32 +137,34 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool ContainsAll(ImmutableArray candidate, ImmutableArray pressedKeys, KeyCombinationMatchingMode matchingMode) { + ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressed = pressedKeys.Select(k => (k, getVirtualKey(k))).ToImmutableArray(); + // first, check that all the candidate keys are contained in the provided pressed keys. // regardless of the matching mode, every key needs to at least be present (matching modes only change // the behaviour of excess keys). foreach (var key in candidate) { - if (!ContainsKey(pressedKeys, key)) + if (!ContainsKey(pressed, key)) return false; } switch (matchingMode) { case KeyCombinationMatchingMode.Exact: - foreach (var key in pressedKeys) + foreach (var key in pressed) { // in exact matching mode, every pressed key needs to be in the candidate. - if (!ContainsKeyPermissive(candidate, key)) + if (!ContainsKeyPermissive(candidate, key.Physical, key.Virtual)) return false; } break; case KeyCombinationMatchingMode.Modifiers: - foreach (var key in pressedKeys) + foreach (var key in pressed) { // in modifiers match mode, the same check applies as exact but only for modifier keys. - if (IsModifierKey(key) && !ContainsKeyPermissive(candidate, key)) + if (IsModifierKey(key.Physical) && !ContainsKeyPermissive(candidate, key.Physical, key.Virtual)) return false; } @@ -157,42 +183,12 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr /// This will match bidirectionally for modifier keys (LShift and Shift being present in both of the two parameters in either order will return true). /// /// The candidate key binding to match against. - /// The key which has been pressed by a user. + /// The key which has been pressed by a user. + /// The virtual key corresponding to the physical key, if any. /// Whether this is a match. - internal static bool ContainsKeyPermissive(ImmutableArray candidate, InputKey key) + internal static bool ContainsKeyPermissive(ImmutableArray candidate, InputKey physicalKey, InputKey? virtualKey) { - switch (key) - { - case InputKey.LControl: - case InputKey.RControl: - if (candidate.Contains(InputKey.Control)) - return true; - - break; - - case InputKey.LShift: - case InputKey.RShift: - if (candidate.Contains(InputKey.Shift)) - return true; - - break; - - case InputKey.RAlt: - case InputKey.LAlt: - if (candidate.Contains(InputKey.Alt)) - return true; - - break; - - case InputKey.LSuper: - case InputKey.RSuper: - if (candidate.Contains(InputKey.Super)) - return true; - - break; - } - - return candidate.Contains(key); + return candidate.Contains(physicalKey) || (virtualKey != null && candidate.Contains(virtualKey.Value)); } /// @@ -202,36 +198,9 @@ internal static bool ContainsKeyPermissive(ImmutableArray candidate, I /// The currently pressed keys to match against. /// The candidate key to check. /// Whether this is a match. - internal static bool ContainsKey(ImmutableArray pressedKeys, InputKey candidateKey) + internal static bool ContainsKey(ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressedKeys, InputKey candidateKey) { - switch (candidateKey) - { - case InputKey.Control: - if (pressedKeys.Contains(InputKey.LControl) || pressedKeys.Contains(InputKey.RControl)) - return true; - - break; - - case InputKey.Shift: - if (pressedKeys.Contains(InputKey.LShift) || pressedKeys.Contains(InputKey.RShift)) - return true; - - break; - - case InputKey.Alt: - if (pressedKeys.Contains(InputKey.LAlt) || pressedKeys.Contains(InputKey.RAlt)) - return true; - - break; - - case InputKey.Super: - if (pressedKeys.Contains(InputKey.LSuper) || pressedKeys.Contains(InputKey.RSuper)) - return true; - - break; - } - - return pressedKeys.Contains(candidateKey); + return pressedKeys.Any(k => candidateKey == k.Physical || candidateKey == k.Virtual); } public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys); From 560172da50a31dd71550a7dc7c434467e9dee029 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 17:53:56 +0100 Subject: [PATCH 016/140] Reword xmldoc --- osu.Framework/Input/Bindings/KeyCombination.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 3d1251b88f..b340e3457d 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -192,8 +192,7 @@ internal static bool ContainsKeyPermissive(ImmutableArray candidate, I } /// - /// Check whether a single key from a candidate binding is relevant to the currently pressed keys. - /// If the contain a left/right specific modifier, the needs to be either that specific modifier or its generic variant to match. + /// Check whether a single physical or virtual key from a candidate binding is relevant to the currently pressed keys. /// /// The currently pressed keys to match against. /// The candidate key to check. From b15194d5db6520aca1a581985c9607c92b44d539 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 27 Mar 2024 20:00:24 +0100 Subject: [PATCH 017/140] Remove old xmldoc --- osu.Framework/Input/Bindings/KeyCombination.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index b340e3457d..abd6e94872 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -180,7 +180,6 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr /// /// Check whether the provided key is part of the candidate binding. - /// This will match bidirectionally for modifier keys (LShift and Shift being present in both of the two parameters in either order will return true). /// /// The candidate key binding to match against. /// The key which has been pressed by a user. From fe629c6050f24c2e8a446ddf5c4b94758fcee21d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 29 Mar 2024 19:58:18 +0100 Subject: [PATCH 018/140] Make the code explicit about physical vs virtual InputKeys --- .../Extensions/InputKeyExtensions.cs | 42 +++++++++++++++++++ .../Input/Bindings/KeyCombination.cs | 12 +++++- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 osu.Framework/Extensions/InputKeyExtensions.cs diff --git a/osu.Framework/Extensions/InputKeyExtensions.cs b/osu.Framework/Extensions/InputKeyExtensions.cs new file mode 100644 index 0000000000..50b83acd0e --- /dev/null +++ b/osu.Framework/Extensions/InputKeyExtensions.cs @@ -0,0 +1,42 @@ +// 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 osu.Framework.Input.Bindings; + +namespace osu.Framework.Extensions +{ + public static class InputKeyExtensions + { + public static bool IsPhysical(this InputKey key) + { + if (!Enum.IsDefined(key) || IsVirtual(key)) + return false; + + switch (key) + { + case InputKey.None: + case InputKey.LastKey: + return false; + + default: + return true; + } + } + + public static bool IsVirtual(this InputKey key) + { + switch (key) + { + case InputKey.Shift: + case InputKey.Control: + case InputKey.Alt: + case InputKey.Super: + return true; + + default: + return false; + } + } + } +} diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index abd6e94872..b86fc7e21f 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; +using osu.Framework.Extensions; using osu.Framework.Input.States; using osuTK; using osuTK.Input; @@ -137,8 +138,11 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool ContainsAll(ImmutableArray candidate, ImmutableArray pressedKeys, KeyCombinationMatchingMode matchingMode) { + Debug.Assert(pressedKeys.All(k => k.IsPhysical())); ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressed = pressedKeys.Select(k => (k, getVirtualKey(k))).ToImmutableArray(); + Debug.Assert(pressed.All(k => k.Virtual == null || k.Virtual.Value.IsVirtual())); + // first, check that all the candidate keys are contained in the provided pressed keys. // regardless of the matching mode, every key needs to at least be present (matching modes only change // the behaviour of excess keys). @@ -198,7 +202,13 @@ internal static bool ContainsKeyPermissive(ImmutableArray candidate, I /// Whether this is a match. internal static bool ContainsKey(ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressedKeys, InputKey candidateKey) { - return pressedKeys.Any(k => candidateKey == k.Physical || candidateKey == k.Virtual); + if (candidateKey.IsPhysical()) + { + return pressedKeys.Any(k => k.Physical == candidateKey); + } + + Debug.Assert(candidateKey.IsVirtual()); + return pressedKeys.Any(k => k.Virtual == candidateKey); } public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys); From 2334498915dc7403cba43eb848a1bf3f980287ed Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 29 Mar 2024 19:58:55 +0100 Subject: [PATCH 019/140] Remove even more invalid tests These may have been useful at some point, but not anymore. --- osu.Framework.Tests/Input/KeyCombinationTest.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Framework.Tests/Input/KeyCombinationTest.cs b/osu.Framework.Tests/Input/KeyCombinationTest.cs index d495dee1d5..efaadf57c1 100644 --- a/osu.Framework.Tests/Input/KeyCombinationTest.cs +++ b/osu.Framework.Tests/Input/KeyCombinationTest.cs @@ -14,19 +14,16 @@ public class KeyCombinationTest // test single combination matches. new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Any, true }, new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true }, - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Any, true }, new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, false }, new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Any, true }, new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Exact, true }, new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Exact, true }, new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, false }, new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Exact, true }, new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.LShift), KeyCombinationMatchingMode.Modifiers, true }, new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, - new object[] { new KeyCombination(InputKey.Shift), new KeyCombination(InputKey.Shift), KeyCombinationMatchingMode.Modifiers, true }, new object[] { new KeyCombination(InputKey.LShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, false }, new object[] { new KeyCombination(InputKey.RShift), new KeyCombination(InputKey.RShift), KeyCombinationMatchingMode.Modifiers, true }, }; From 588fd36203d9092352ad9da2e3a68820bbfd309e Mon Sep 17 00:00:00 2001 From: Susko3 Date: Fri, 29 Mar 2024 20:58:43 +0100 Subject: [PATCH 020/140] Rename methods Could be better but it's a start. --- osu.Framework/Input/Bindings/KeyCombination.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index b86fc7e21f..1be0130ab8 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -148,7 +148,7 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr // the behaviour of excess keys). foreach (var key in candidate) { - if (!ContainsKey(pressed, key)) + if (!IsPressed(pressed, key)) return false; } @@ -158,7 +158,7 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr foreach (var key in pressed) { // in exact matching mode, every pressed key needs to be in the candidate. - if (!ContainsKeyPermissive(candidate, key.Physical, key.Virtual)) + if (!ContainsKey(candidate, key.Physical, key.Virtual)) return false; } @@ -168,7 +168,7 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr foreach (var key in pressed) { // in modifiers match mode, the same check applies as exact but only for modifier keys. - if (IsModifierKey(key.Physical) && !ContainsKeyPermissive(candidate, key.Physical, key.Virtual)) + if (IsModifierKey(key.Physical) && !ContainsKey(candidate, key.Physical, key.Virtual)) return false; } @@ -189,7 +189,7 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr /// The key which has been pressed by a user. /// The virtual key corresponding to the physical key, if any. /// Whether this is a match. - internal static bool ContainsKeyPermissive(ImmutableArray candidate, InputKey physicalKey, InputKey? virtualKey) + internal static bool ContainsKey(ImmutableArray candidate, InputKey physicalKey, InputKey? virtualKey) { return candidate.Contains(physicalKey) || (virtualKey != null && candidate.Contains(virtualKey.Value)); } @@ -200,7 +200,7 @@ internal static bool ContainsKeyPermissive(ImmutableArray candidate, I /// The currently pressed keys to match against. /// The candidate key to check. /// Whether this is a match. - internal static bool ContainsKey(ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressedKeys, InputKey candidateKey) + internal static bool IsPressed(ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressedKeys, InputKey candidateKey) { if (candidateKey.IsPhysical()) { From e21e951059c83c68d5219822f2d345f0a141d93d Mon Sep 17 00:00:00 2001 From: Susko3 Date: Wed, 3 Apr 2024 11:40:27 +0200 Subject: [PATCH 021/140] Use more descriptive names Per bdach's suggestion. --- .../Input/Bindings/KeyCombination.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 1be0130ab8..83ce7faf96 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -131,22 +131,22 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat /// /// Check whether the provided set of pressed keys matches the candidate binding. /// - /// The candidate key binding to match against. - /// The keys which have been pressed by a user. + /// The candidate key binding to match against. + /// The keys which have been pressed by a user. /// The matching mode to be used when checking. /// Whether this is a match. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool ContainsAll(ImmutableArray candidate, ImmutableArray pressedKeys, KeyCombinationMatchingMode matchingMode) + internal static bool ContainsAll(ImmutableArray candidateKeyBinding, ImmutableArray pressedPhysicalKeys, KeyCombinationMatchingMode matchingMode) { - Debug.Assert(pressedKeys.All(k => k.IsPhysical())); - ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressed = pressedKeys.Select(k => (k, getVirtualKey(k))).ToImmutableArray(); + Debug.Assert(pressedPhysicalKeys.All(k => k.IsPhysical())); + ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressed = pressedPhysicalKeys.Select(k => (k, getVirtualKey(k))).ToImmutableArray(); Debug.Assert(pressed.All(k => k.Virtual == null || k.Virtual.Value.IsVirtual())); // first, check that all the candidate keys are contained in the provided pressed keys. // regardless of the matching mode, every key needs to at least be present (matching modes only change // the behaviour of excess keys). - foreach (var key in candidate) + foreach (var key in candidateKeyBinding) { if (!IsPressed(pressed, key)) return false; @@ -158,7 +158,7 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr foreach (var key in pressed) { // in exact matching mode, every pressed key needs to be in the candidate. - if (!ContainsKey(candidate, key.Physical, key.Virtual)) + if (!KeyBindingContains(candidateKeyBinding, key)) return false; } @@ -168,7 +168,7 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr foreach (var key in pressed) { // in modifiers match mode, the same check applies as exact but only for modifier keys. - if (IsModifierKey(key.Physical) && !ContainsKey(candidate, key.Physical, key.Virtual)) + if (IsModifierKey(key.Physical) && !KeyBindingContains(candidateKeyBinding, key)) return false; } @@ -185,13 +185,12 @@ internal static bool ContainsAll(ImmutableArray candidate, ImmutableAr /// /// Check whether the provided key is part of the candidate binding. /// - /// The candidate key binding to match against. - /// The key which has been pressed by a user. - /// The virtual key corresponding to the physical key, if any. + /// The candidate key binding to match against. + /// Tuple of a physical key that has been pressed by a user and its corresponding virtual key (if any). /// Whether this is a match. - internal static bool ContainsKey(ImmutableArray candidate, InputKey physicalKey, InputKey? virtualKey) + internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, (InputKey Physical, InputKey? Virtual) key) { - return candidate.Contains(physicalKey) || (virtualKey != null && candidate.Contains(virtualKey.Value)); + return candidateKeyBinding.Contains(key.Physical) || (key.Virtual != null && candidateKeyBinding.Contains(key.Virtual.Value)); } /// From 3a6017e5871ca5294bcd4f45ea479372c9dba604 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 22 Apr 2024 14:23:40 +0200 Subject: [PATCH 022/140] Refactor to call `getVirtualKey()` as needed Co-authored-by: Dan Balasescu --- .../Input/Bindings/KeyCombination.cs | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 83ce7faf96..e9e1af88a8 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -139,23 +139,20 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat internal static bool ContainsAll(ImmutableArray candidateKeyBinding, ImmutableArray pressedPhysicalKeys, KeyCombinationMatchingMode matchingMode) { Debug.Assert(pressedPhysicalKeys.All(k => k.IsPhysical())); - ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressed = pressedPhysicalKeys.Select(k => (k, getVirtualKey(k))).ToImmutableArray(); - - Debug.Assert(pressed.All(k => k.Virtual == null || k.Virtual.Value.IsVirtual())); // first, check that all the candidate keys are contained in the provided pressed keys. // regardless of the matching mode, every key needs to at least be present (matching modes only change // the behaviour of excess keys). foreach (var key in candidateKeyBinding) { - if (!IsPressed(pressed, key)) + if (!IsPressed(pressedPhysicalKeys, key)) return false; } switch (matchingMode) { case KeyCombinationMatchingMode.Exact: - foreach (var key in pressed) + foreach (var key in pressedPhysicalKeys) { // in exact matching mode, every pressed key needs to be in the candidate. if (!KeyBindingContains(candidateKeyBinding, key)) @@ -165,10 +162,10 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I break; case KeyCombinationMatchingMode.Modifiers: - foreach (var key in pressed) + foreach (var key in pressedPhysicalKeys) { // in modifiers match mode, the same check applies as exact but only for modifier keys. - if (IsModifierKey(key.Physical) && !KeyBindingContains(candidateKeyBinding, key)) + if (IsModifierKey(key) && !KeyBindingContains(candidateKeyBinding, key)) return false; } @@ -186,28 +183,27 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I /// Check whether the provided key is part of the candidate binding. /// /// The candidate key binding to match against. - /// Tuple of a physical key that has been pressed by a user and its corresponding virtual key (if any). + /// The physical key that has been pressed. /// Whether this is a match. - internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, (InputKey Physical, InputKey? Virtual) key) + internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, InputKey physicalKey) { - return candidateKeyBinding.Contains(key.Physical) || (key.Virtual != null && candidateKeyBinding.Contains(key.Virtual.Value)); + return candidateKeyBinding.Contains(physicalKey) + || (getVirtualKey(physicalKey) is InputKey vKey && candidateKeyBinding.Contains(vKey)); } /// /// Check whether a single physical or virtual key from a candidate binding is relevant to the currently pressed keys. /// - /// The currently pressed keys to match against. + /// The currently pressed keys to match against. /// The candidate key to check. /// Whether this is a match. - internal static bool IsPressed(ImmutableArray<(InputKey Physical, InputKey? Virtual)> pressedKeys, InputKey candidateKey) + internal static bool IsPressed(ImmutableArray pressedPhysicalKeys, InputKey candidateKey) { if (candidateKey.IsPhysical()) - { - return pressedKeys.Any(k => k.Physical == candidateKey); - } + return pressedPhysicalKeys.Contains(candidateKey); Debug.Assert(candidateKey.IsVirtual()); - return pressedKeys.Any(k => k.Virtual == candidateKey); + return pressedPhysicalKeys.Any(k => getVirtualKey(k) == candidateKey); } public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys); From f3fd47646f2f96b3ad46199edd2d81ba9f13c7d8 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 22 Apr 2024 14:26:20 +0200 Subject: [PATCH 023/140] Add `InputState` param to `getVirtualKey()` To be used in the future. --- .../Input/KeyCombinationTest.cs | 3 +- ...TestSceneReadableKeyCombinationProvider.cs | 4 +-- .../Input/Bindings/KeyBindingContainer.cs | 6 ++-- .../Input/Bindings/KeyCombination.cs | 28 +++++++++++-------- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/osu.Framework.Tests/Input/KeyCombinationTest.cs b/osu.Framework.Tests/Input/KeyCombinationTest.cs index efaadf57c1..40099a9976 100644 --- a/osu.Framework.Tests/Input/KeyCombinationTest.cs +++ b/osu.Framework.Tests/Input/KeyCombinationTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Input.Bindings; +using osu.Framework.Input.States; namespace osu.Framework.Tests.Input { @@ -30,7 +31,7 @@ public class KeyCombinationTest [TestCaseSource(nameof(key_combination_display_test_cases))] public void TestLeftRightModifierHandling(KeyCombination candidate, KeyCombination pressed, KeyCombinationMatchingMode matchingMode, bool shouldContain) - => Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, matchingMode)); + => Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, new InputState(), matchingMode)); [Test] public void TestCreationNoDuplicates() diff --git a/osu.Framework.Tests/Visual/Input/TestSceneReadableKeyCombinationProvider.cs b/osu.Framework.Tests/Visual/Input/TestSceneReadableKeyCombinationProvider.cs index 89df608755..21d8e43af4 100644 --- a/osu.Framework.Tests/Visual/Input/TestSceneReadableKeyCombinationProvider.cs +++ b/osu.Framework.Tests/Visual/Input/TestSceneReadableKeyCombinationProvider.cs @@ -139,7 +139,7 @@ protected override void LoadComplete() protected override bool OnKeyDown(KeyDownEvent e) { - if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), KeyCombinationMatchingMode.Any)) + if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), e.CurrentState, KeyCombinationMatchingMode.Any)) box.Colour = Color4.Navy; return base.OnKeyDown(e); @@ -147,7 +147,7 @@ protected override bool OnKeyDown(KeyDownEvent e) protected override void OnKeyUp(KeyUpEvent e) { - if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), KeyCombinationMatchingMode.Any)) + if (keyCombination.IsPressed(new KeyCombination(KeyCombination.FromKey(e.Key)), e.CurrentState, KeyCombinationMatchingMode.Any)) box.Colour = Color4.DarkGray; base.OnKeyUp(e); diff --git a/osu.Framework/Input/Bindings/KeyBindingContainer.cs b/osu.Framework/Input/Bindings/KeyBindingContainer.cs index 39c1a0fe7c..5f0149ded2 100644 --- a/osu.Framework/Input/Bindings/KeyBindingContainer.cs +++ b/osu.Framework/Input/Bindings/KeyBindingContainer.cs @@ -224,7 +224,7 @@ private bool handleNewPressed(InputState state, InputKey newKey, Vector2? scroll if (pressedBindings.Contains(binding)) continue; - if (binding.KeyCombination.IsPressed(pressedCombination, matchingMode)) + if (binding.KeyCombination.IsPressed(pressedCombination, state, matchingMode)) newlyPressed.Add(binding); } } @@ -251,7 +251,7 @@ private bool handleNewPressed(InputState state, InputKey newKey, Vector2? scroll if (simultaneousMode == SimultaneousBindingMode.None && (matchingMode == KeyCombinationMatchingMode.Exact || matchingMode == KeyCombinationMatchingMode.Modifiers)) { // only want to release pressed actions if no existing bindings would still remain pressed - if (pressedBindings.Count > 0 && !pressedBindings.Any(m => m.KeyCombination.IsPressed(pressedCombination, matchingMode))) + if (pressedBindings.Count > 0 && !pressedBindings.Any(m => m.KeyCombination.IsPressed(pressedCombination, state, matchingMode))) releasePressedActions(state); } @@ -365,7 +365,7 @@ private void handleNewReleased(InputState state, InputKey releasedKey) { var binding = pressedBindings[i]; - if (pressedInputKeys.Count == 0 || !binding.KeyCombination.IsPressed(pressedCombination, KeyCombinationMatchingMode.Any)) + if (pressedInputKeys.Count == 0 || !binding.KeyCombination.IsPressed(pressedCombination, state, KeyCombinationMatchingMode.Any)) { pressedBindings.RemoveAt(i--); PropagateReleased(getInputQueue(binding).Where(d => d.IsRootedAt(this)), state, binding.GetAction()); diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index e9e1af88a8..8778604127 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -92,19 +92,20 @@ private KeyCombination(ImmutableArray keys) /// Check whether the provided pressed keys are valid for this . /// /// The potential pressed keys for this . + /// The current input state. /// The method for handling exact key matches. /// Whether the pressedKeys keys are valid. - public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode matchingMode) + public bool IsPressed(KeyCombination pressedKeys, InputState inputState, KeyCombinationMatchingMode matchingMode) { Debug.Assert(!pressedKeys.Keys.Contains(InputKey.None)); // Having None in pressed keys will break IsPressed if (Keys == pressedKeys.Keys) // Fast test for reference equality of underlying array return true; - return ContainsAll(Keys, pressedKeys.Keys, matchingMode); + return ContainsAll(Keys, pressedKeys.Keys, inputState, matchingMode); } - private static InputKey? getVirtualKey(InputKey key) + private static InputKey? getVirtualKey(InputKey key, InputState inputState) { switch (key) { @@ -133,10 +134,11 @@ public bool IsPressed(KeyCombination pressedKeys, KeyCombinationMatchingMode mat /// /// The candidate key binding to match against. /// The keys which have been pressed by a user. + /// The current input state. /// The matching mode to be used when checking. /// Whether this is a match. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool ContainsAll(ImmutableArray candidateKeyBinding, ImmutableArray pressedPhysicalKeys, KeyCombinationMatchingMode matchingMode) + internal static bool ContainsAll(ImmutableArray candidateKeyBinding, ImmutableArray pressedPhysicalKeys, InputState inputState, KeyCombinationMatchingMode matchingMode) { Debug.Assert(pressedPhysicalKeys.All(k => k.IsPhysical())); @@ -145,7 +147,7 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I // the behaviour of excess keys). foreach (var key in candidateKeyBinding) { - if (!IsPressed(pressedPhysicalKeys, key)) + if (!IsPressed(pressedPhysicalKeys, key, inputState)) return false; } @@ -155,7 +157,7 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I foreach (var key in pressedPhysicalKeys) { // in exact matching mode, every pressed key needs to be in the candidate. - if (!KeyBindingContains(candidateKeyBinding, key)) + if (!KeyBindingContains(candidateKeyBinding, key, inputState)) return false; } @@ -165,7 +167,7 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I foreach (var key in pressedPhysicalKeys) { // in modifiers match mode, the same check applies as exact but only for modifier keys. - if (IsModifierKey(key) && !KeyBindingContains(candidateKeyBinding, key)) + if (IsModifierKey(key) && !KeyBindingContains(candidateKeyBinding, key, inputState)) return false; } @@ -184,11 +186,12 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I /// /// The candidate key binding to match against. /// The physical key that has been pressed. + /// The current input state. /// Whether this is a match. - internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, InputKey physicalKey) + internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, InputKey physicalKey, InputState inputState) { - return candidateKeyBinding.Contains(physicalKey) - || (getVirtualKey(physicalKey) is InputKey vKey && candidateKeyBinding.Contains(vKey)); + return candidateKeyBinding.Contains(physicalKey) || + (getVirtualKey(physicalKey, inputState) is InputKey vKey && candidateKeyBinding.Contains(vKey)); } /// @@ -196,14 +199,15 @@ internal static bool KeyBindingContains(ImmutableArray candidateKeyBin /// /// The currently pressed keys to match against. /// The candidate key to check. + /// The current input state. /// Whether this is a match. - internal static bool IsPressed(ImmutableArray pressedPhysicalKeys, InputKey candidateKey) + internal static bool IsPressed(ImmutableArray pressedPhysicalKeys, InputKey candidateKey, InputState inputState) { if (candidateKey.IsPhysical()) return pressedPhysicalKeys.Contains(candidateKey); Debug.Assert(candidateKey.IsVirtual()); - return pressedPhysicalKeys.Any(k => getVirtualKey(k) == candidateKey); + return pressedPhysicalKeys.Any(k => getVirtualKey(k, inputState) == candidateKey); } public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys); From 98da3979626a9fd8ba211852feeec7cd08b7592f Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 19 Apr 2024 22:29:18 +0200 Subject: [PATCH 024/140] Remove Linux hwaccel features from FFmpeg build --- .github/workflows/build-ffmpeg.yml | 2 +- osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/build-ffmpeg.yml b/.github/workflows/build-ffmpeg.yml index aebd3de365..3331b0b817 100644 --- a/.github/workflows/build-ffmpeg.yml +++ b/.github/workflows/build-ffmpeg.yml @@ -112,7 +112,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install nasm libva-dev libvdpau-dev + sudo apt-get install nasm - name: Checkout uses: actions/checkout@v4 diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh index d3d52129b2..7056c59d84 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh @@ -7,13 +7,6 @@ popd > /dev/null source "$SCRIPT_PATH/common.sh" FFMPEG_FLAGS+=( - # --enable-vaapi - # --enable-vdpau - # --enable-hwaccel='h264_vaapi,h264_vdpau' - # --enable-hwaccel='hevc_vaapi,hevc_vdpau' - # --enable-hwaccel='vp8_vaapi,vp8_vdpau' - # --enable-hwaccel='vp9_vaapi,vp9_vdpau' - --target-os=linux ) From eef0206043bb0e04e05907e7fdfed6d2d11c5ec8 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 19 Apr 2024 22:30:30 +0200 Subject: [PATCH 025/140] Add FFmpeg build artifacts to .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index d665f2912f..3f1822acbc 100644 --- a/.gitignore +++ b/.gitignore @@ -339,3 +339,8 @@ inspectcode .idea/.idea.osu-framework.Desktop/.idea/misc.xml .idea/.idea.osu-framework.Android/.idea/deploymentTargetDropDown.xml + +# NativeLibs build folders and tarballs +osu.Framework.NativeLibs/scripts/ffmpeg/*/ +osu.Framework.NativeLibs/scripts/ffmpeg/*.tar.gz + From da8af601210eaed813d129b675a923553933dadc Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 19 Apr 2024 22:32:13 +0200 Subject: [PATCH 026/140] Update FFmpeg build to 7.0 and remove old workarounds --- .../scripts/ffmpeg/build-linux.sh | 4 - .../scripts/ffmpeg/build-win.sh | 9 --- .../scripts/ffmpeg/common.sh | 2 +- .../scripts/ffmpeg/fix-binutils-2.41.patch | 76 ------------------- 4 files changed, 1 insertion(+), 90 deletions(-) delete mode 100644 osu.Framework.NativeLibs/scripts/ffmpeg/fix-binutils-2.41.patch diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh index 7056c59d84..5044d4e061 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/build-linux.sh @@ -12,10 +12,6 @@ FFMPEG_FLAGS+=( pushd . > /dev/null prep_ffmpeg linux-x64 -# Apply patch from upstream to fix errors with new binutils versions: -# Ticket: https://fftrac-bg.ffmpeg.org/ticket/10405 -# This patch should be removed when FFmpeg is updated to >=6.1 -patch -p1 < "$SCRIPT_PATH/fix-binutils-2.41.patch" build_ffmpeg popd > /dev/null diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/build-win.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/build-win.sh index ded8520dab..8dff16f670 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/build-win.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/build-win.sh @@ -54,15 +54,6 @@ FFMPEG_FLAGS+=( pushd . > /dev/null prep_ffmpeg "win-$arch" - -# FFmpeg doesn't do this correctly when building, so we do it instead. -# A newer FFmpeg release might make this unnecessary. -echo '-> Creating resource objects...' -make .version -for res in lib*/*res.rc; do - "${cross_prefix}windres" -I. "$res" "${res%.rc}.o" -done - build_ffmpeg popd > /dev/null diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh index 7753867dca..fc83713280 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eu -FFMPEG_VERSION=4.3.3 +FFMPEG_VERSION="7.0" FFMPEG_FILE="ffmpeg-$FFMPEG_VERSION.tar.gz" FFMPEG_FLAGS=( # General options diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/fix-binutils-2.41.patch b/osu.Framework.NativeLibs/scripts/ffmpeg/fix-binutils-2.41.patch deleted file mode 100644 index 33fd3d484f..0000000000 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/fix-binutils-2.41.patch +++ /dev/null @@ -1,76 +0,0 @@ -From effadce6c756247ea8bae32dc13bb3e6f464f0eb Mon Sep 17 00:00:00 2001 -From: =?utf8?q?R=C3=A9mi=20Denis-Courmont?= -Date: Sun, 16 Jul 2023 18:18:02 +0300 -Subject: [PATCH] avcodec/x86/mathops: clip constants used with shift - instructions within inline assembly - -Fixes assembling with binutil as >= 2.41 - -Signed-off-by: James Almer ---- - libavcodec/x86/mathops.h | 26 +++++++++++++++++++++++--- - 1 file changed, 23 insertions(+), 3 deletions(-) - -diff --git a/libavcodec/x86/mathops.h b/libavcodec/x86/mathops.h -index 6298f5ed19..ca7e2dffc1 100644 ---- a/libavcodec/x86/mathops.h -+++ b/libavcodec/x86/mathops.h -@@ -35,12 +35,20 @@ - static av_always_inline av_const int MULL(int a, int b, unsigned shift) - { - int rt, dummy; -+ if (__builtin_constant_p(shift)) - __asm__ ( - "imull %3 \n\t" - "shrdl %4, %%edx, %%eax \n\t" - :"=a"(rt), "=d"(dummy) -- :"a"(a), "rm"(b), "ci"((uint8_t)shift) -+ :"a"(a), "rm"(b), "i"(shift & 0x1F) - ); -+ else -+ __asm__ ( -+ "imull %3 \n\t" -+ "shrdl %4, %%edx, %%eax \n\t" -+ :"=a"(rt), "=d"(dummy) -+ :"a"(a), "rm"(b), "c"((uint8_t)shift) -+ ); - return rt; - } - -@@ -113,19 +121,31 @@ __asm__ volatile(\ - // avoid +32 for shift optimization (gcc should do that ...) - #define NEG_SSR32 NEG_SSR32 - static inline int32_t NEG_SSR32( int32_t a, int8_t s){ -+ if (__builtin_constant_p(s)) - __asm__ ("sarl %1, %0\n\t" - : "+r" (a) -- : "ic" ((uint8_t)(-s)) -+ : "i" (-s & 0x1F) - ); -+ else -+ __asm__ ("sarl %1, %0\n\t" -+ : "+r" (a) -+ : "c" ((uint8_t)(-s)) -+ ); - return a; - } - - #define NEG_USR32 NEG_USR32 - static inline uint32_t NEG_USR32(uint32_t a, int8_t s){ -+ if (__builtin_constant_p(s)) - __asm__ ("shrl %1, %0\n\t" - : "+r" (a) -- : "ic" ((uint8_t)(-s)) -+ : "i" (-s & 0x1F) - ); -+ else -+ __asm__ ("shrl %1, %0\n\t" -+ : "+r" (a) -+ : "c" ((uint8_t)(-s)) -+ ); - return a; - } - --- -2.30.2 - From 12a03a5b625d12f390a7ef10b394c0bdc39fd580 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Fri, 19 Apr 2024 22:56:30 +0200 Subject: [PATCH 027/140] Explicitly disable debug symbols in FFmpeg build These were previously stripped from the binaries, but don't have to be generated in the first place. --- osu.Framework.NativeLibs/scripts/ffmpeg/common.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh index fc83713280..efd7c954b6 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh @@ -7,6 +7,7 @@ FFMPEG_FLAGS=( # General options --disable-static --enable-shared + --disable-debug --disable-all --disable-autodetect --enable-lto From 545688703320e4f7c953f7d321027a6e7f9121f2 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Mon, 22 Apr 2024 21:21:13 +0200 Subject: [PATCH 028/140] Add Android FFmpeg build - Use Android NDK preinstalled in GHA - Target ARMv7-A, ARMv8-A and (mostly for emulators) x86, x86_64 - Target API level 21 (Android 5+) - Always enable NEON (requirement for API level 21) - Collect binaries in osu.Framework.Android so they're bundled into the AAR/APK --- .github/workflows/build-ffmpeg.yml | 43 +++++++++ .../scripts/ffmpeg/build-android.sh | 96 +++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100755 osu.Framework.NativeLibs/scripts/ffmpeg/build-android.sh diff --git a/.github/workflows/build-ffmpeg.yml b/.github/workflows/build-ffmpeg.yml index 3331b0b817..ab2c38496d 100644 --- a/.github/workflows/build-ffmpeg.yml +++ b/.github/workflows/build-ffmpeg.yml @@ -126,6 +126,32 @@ jobs: name: linux-x64 path: linux-x64 + build-android: + name: Build Android + runs-on: ubuntu-22.04 + strategy: + matrix: + arch: + - armeabi-v7a + - arm64-v8a + - x86 + - x86_64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build + run: osu.Framework.NativeLibs/scripts/ffmpeg/build-android.sh + env: + arch: ${{ matrix.arch }} + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: android-${{ matrix.arch }} + path: android-${{ matrix.arch }} + make-pr: name: Create pull request runs-on: ubuntu-22.04 @@ -134,6 +160,7 @@ jobs: - build-win - build-win-arm64 - build-linux + - build-android steps: - name: Checkout uses: actions/checkout@v4 @@ -158,6 +185,22 @@ jobs: with: name: win-x86 path: osu.Framework.NativeLibs/runtimes/win-x86/native + - uses: actions/download-artifact@v4 + with: + name: android-armeabi-v7a + path: osu.Framework.Android/armeabi-v7a + - uses: actions/download-artifact@v4 + with: + name: android-arm64-v8a + path: osu.Framework.Android/arm64-v8a + - uses: actions/download-artifact@v4 + with: + name: android-x86 + path: osu.Framework.Android/x86 + - uses: actions/download-artifact@v4 + with: + name: android-x86_64 + path: osu.Framework.Android/x86_64 - uses: peter-evans/create-pull-request@v6 with: diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/build-android.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/build-android.sh new file mode 100755 index 0000000000..73431886e6 --- /dev/null +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/build-android.sh @@ -0,0 +1,96 @@ +#!/bin/bash +set -eu + +# Android ABI level to target. 21 is the minimum supported by the NDK +# See https://apilevels.com for info on what the API level means +API_LEVEL="21" + +if [ -z "${ANDROID_NDK_ROOT:-}" ]; then + echo "ANDROID_NDK_ROOT must be set" + exit 1 +fi + +pushd "$(dirname "$0")" > /dev/null +SCRIPT_PATH=$(pwd) +popd > /dev/null +source "$SCRIPT_PATH/common.sh" + +if [ -z "${arch-}" ]; then + PS3='Build for which arch? ' + select arch in "armeabi-v7a" "arm64-v8a" "x86" "x86_64"; do + if [ -z "$arch" ]; then + echo "invalid option" + else + break + fi + done +fi + +cpu='' +cross_arch='' +cc='' +cflags='' +asm_options='' + +case $arch in + armeabi-v7a) + cpu='armv7-a' + cross_arch='armv7-a' + cc="armv7a-linux-androideabi${API_LEVEL}-clang" + cflags='-mfpu=neon -mfloat-abi=softfp' + asm_options='--enable-neon --enable-asm --enable-inline-asm' + ;; + + arm64-v8a) + cpu='armv8-a' + cross_arch='aarch64' + cc="aarch64-linux-android${API_LEVEL}-clang" + asm_options='--enable-neon --enable-asm --enable-inline-asm' + ;; + + x86) + cpu='i686' + cross_arch='i686' + cc="i686-linux-android${API_LEVEL}-clang" + # ASM has text relocations + asm_options='--disable-asm' + ;; + + x86_64) + cpu='x86-64' + cross_arch='x86_64' + cc="x86_64-linux-android${API_LEVEL}-clang" + asm_options='--enable-asm --enable-inline-asm' + ;; +esac + +toolchain_path="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64" +bin_path="$toolchain_path/bin" + +FFMPEG_FLAGS+=( + --enable-jni + + --enable-cross-compile + --target-os=android + --cpu=$cpu + --arch=$cross_arch + --sysroot="$toolchain_path/sysroot" + --cc="$bin_path/$cc" + --cxx="$bin_path/$cc++" + --ld="$bin_path/$cc" + --ar="$bin_path/llvm-ar" + --as="$bin_path/$cc" + --nm="$bin_path/llvm-nm" + --ranlib="$bin_path/llvm-ranlib" + --strip="$bin_path/llvm-strip" + --x86asmexe="$bin_path/yasm" + --extra-cflags="-fstrict-aliasing -fPIC -DANDROID -D__ANDROID__ $cflags" + + $asm_options +) + +pushd . > /dev/null +prep_ffmpeg "android-$arch" +build_ffmpeg +popd > /dev/null + From 773064e38470526fbc36741bf4da5b26c8c452bd Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Tue, 23 Apr 2024 12:36:16 +0200 Subject: [PATCH 029/140] Add iOS build script --- .github/workflows/build-ffmpeg.yml | 30 +++++++ .../scripts/ffmpeg/build-iOS.sh | 84 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100755 osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh diff --git a/.github/workflows/build-ffmpeg.yml b/.github/workflows/build-ffmpeg.yml index ab2c38496d..723e0804bc 100644 --- a/.github/workflows/build-ffmpeg.yml +++ b/.github/workflows/build-ffmpeg.yml @@ -54,6 +54,36 @@ jobs: name: macOS-universal path: macOS-universal + build-iOS: + name: Build iOS + runs-on: macos-12 + strategy: + matrix: + arch: [arm64, simulator-arm64, simulator-x86_64] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: ilammy/setup-nasm@v1 + if: matrix.arch == 'simulator-x86_64' + + - name: Setup gas-preprocessor + run: | + git clone --depth=1 https://github.com/FFmpeg/gas-preprocessor + echo "GAS_PREPROCESSOR=$PWD/gas-preprocessor/gas-preprocessor.pl" >> $GITHUB_ENV + + - name: Build + run: osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh + env: + arch: ${{ matrix.arch }} + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: iOS-${{ matrix.arch }} + path: iOS-${{ matrix.arch }} + build-win: name: Build Windows runs-on: ubuntu-22.04 diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh new file mode 100755 index 0000000000..957d04142b --- /dev/null +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh @@ -0,0 +1,84 @@ +#!/bin/bash +set -eu + +# Minimum iOS version. This should be the same as in osu.Framework.iOS.csproj +DEPLOYMENT_TARGET="13.4" + +pushd "$(dirname "$0")" > /dev/null +SCRIPT_PATH=$(pwd) +popd > /dev/null +source "$SCRIPT_PATH/common.sh" + +if [ -z "${GAS_PREPROCESSOR:-}" ]; then + echo "GAS_PREPROCESSOR must be set" + exit 1 +fi + +if [ -z "${arch-}" ]; then + PS3='Build for which arch? ' + select arch in "arm64" "simulator-arm64" "simulator-x86_64"; do + if [ -z "$arch" ]; then + echo "invalid option" + else + break + fi + done +fi + +cpu='' +cross_arch='' +cc='' +as='' +cflags='' + +case $arch in + arm64) + cpu='armv8-a' + cross_arch='arm64' + cc='xcrun -sdk iphoneos clang' + as="$GAS_PREPROCESSOR -arch arm64 -- $cc" + cflags="-mios-version-min=$DEPLOYMENT_TARGET" + ;; + + simulator-arm64) + cpu='armv8-a' + cross_arch='arm64' + cc='xcrun -sdk iphonesimulator clang' + as="$GAS_PREPROCESSOR -arch arm64 -- $cc" + cflags="-mios-simulator-version-min=$DEPLOYMENT_TARGET" + ;; + + simulator-x86_64) + cpu='x86-64' + cross_arch='x86_64' + cc='xcrun -sdk iphonesimulator clang' + as="$GAS_PREPROCESSOR -- $cc" + cflags="-mios-simulator-version-min=$DEPLOYMENT_TARGET" + ;; +esac + +FFMPEG_FLAGS+=( + --enable-pic + --enable-videotoolbox + --enable-hwaccel=h264_videotoolbox + --enable-hwaccel=hevc_videotoolbox + --enable-hwaccel=vp9_videotoolbox + + --enable-cross-compile + --target-os=darwin + --cpu=$cpu + --arch=$cross_arch + --cc="$cc" + --as="$as" + --extra-cflags="-arch $cross_arch $cflags" + --extra-ldflags="-arch $cross_arch $cflags" +) + +pushd . > /dev/null +prep_ffmpeg "iOS-$arch" +build_ffmpeg +popd > /dev/null + +# Remove symlinks, keep only libraries with full version in their name +find "iOS-$arch" -type l -delete + From 883142d114f697700debae41f6d6c7543dc43c48 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Tue, 23 Apr 2024 12:56:59 +0200 Subject: [PATCH 030/140] Add script to create iOS XCFrameworks --- .github/workflows/build-ffmpeg.yml | 42 +++++++++++++++ .../scripts/ffmpeg/combine_dylibs.sh | 19 +++++-- .../scripts/ffmpeg/create-xcframeworks.sh | 54 +++++++++++++++++++ 3 files changed, 111 insertions(+), 4 deletions(-) create mode 100755 osu.Framework.NativeLibs/scripts/ffmpeg/create-xcframeworks.sh diff --git a/.github/workflows/build-ffmpeg.yml b/.github/workflows/build-ffmpeg.yml index 723e0804bc..c8812e0a5e 100644 --- a/.github/workflows/build-ffmpeg.yml +++ b/.github/workflows/build-ffmpeg.yml @@ -47,6 +47,8 @@ jobs: - name: Combine run: osu.Framework.NativeLibs/scripts/ffmpeg/combine_dylibs.sh + env: + platform: macOS - name: Upload uses: actions/upload-artifact@v4 @@ -84,6 +86,41 @@ jobs: name: iOS-${{ matrix.arch }} path: iOS-${{ matrix.arch }} + combine-iOS: + name: Combine iOS libs + runs-on: macos-12 + needs: build-iOS + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: iOS-simulator-x86_64 + path: iOS-simulator-x86_64 + - uses: actions/download-artifact@v4 + with: + name: iOS-simulator-arm64 + path: iOS-simulator-arm64 + - uses: actions/download-artifact@v4 + with: + name: iOS-arm64 + path: iOS-arm64 + + - name: Combine dylibs + run: osu.Framework.NativeLibs/scripts/ffmpeg/combine_dylibs.sh + env: + platform: iOS-simulator + + - name: Create XCFrameworks + run: osu.Framework.NativeLibs/scripts/ffmpeg/create-xcframeworks.sh + + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: iOS-xcframework + path: iOS-xcframework + build-win: name: Build Windows runs-on: ubuntu-22.04 @@ -187,6 +224,7 @@ jobs: runs-on: ubuntu-22.04 needs: - combine-macos + - combine-iOS - build-win - build-win-arm64 - build-linux @@ -199,6 +237,10 @@ jobs: with: name: macOS-universal path: osu.Framework.NativeLibs/runtimes/osx/native + - uses: actions/download-artifact@v4 + with: + name: iOS-xcframework + path: osu.Framework.iOS/runtimes/ios/native - uses: actions/download-artifact@v4 with: name: linux-x64 diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/combine_dylibs.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/combine_dylibs.sh index 9eb2ae0372..6965a54499 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/combine_dylibs.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/combine_dylibs.sh @@ -1,13 +1,24 @@ #!/bin/bash set -eu +if [ -z "${platform-}" ]; then + PS3='Combine binaries for which platform? ' + select platform in "macOS" "iOS"; do + if [ -z "$platform" ]; then + echo "invalid option" + else + break + fi + done +fi + pushd . > /dev/null -mkdir -p macOS-universal -cd macOS-arm64 +mkdir -p $platform-universal +cd $platform-arm64 for lib_arm in *.dylib; do - lib_x86="../macOS-x86_64/$lib_arm" + lib_x86="../$platform-x86_64/$lib_arm" echo "-> Creating universal $lib_arm..." - lipo -create "$lib_arm" "$lib_x86" -output "../macOS-universal/$lib_arm" + lipo -create "$lib_arm" "$lib_x86" -output "../$platform-universal/$lib_arm" done popd > /dev/null diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/create-xcframeworks.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/create-xcframeworks.sh new file mode 100755 index 0000000000..68629e4463 --- /dev/null +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/create-xcframeworks.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -eu + +# See build-iOS.sh +DEPLOYMENT_TARGET="13.4" + +for arch in "arm64" "simulator-universal"; do + pushd . > /dev/null + cd "iOS-$arch" + for f in *.*.*.*.dylib; do + [ -f "$f" ] || continue + + # [avcodec].58.10.72.dylib + lib_name="${f%.*.*.*.*}" + + # avcodec.[58.10.72].dylib + tmp=${f#*.} + version_string="${tmp%.*}" + + framework_dir="$lib_name.framework" + mkdir "$framework_dir" + + mv -v "$f" "$framework_dir/$lib_name" + + plist_file="$framework_dir/Info.plist" + + plutil -create xml1 "$plist_file" + plutil -insert CFBundleDevelopmentRegion -string en "$plist_file" + plutil -insert CFBundleExecutable -string "$lib_name" "$plist_file" + plutil -insert CFBundleIdentifier -string "sh.ppy.osu.Framework.iOS.$lib_name" "$plist_file" + plutil -insert CFBundleInfoDictionaryVersion -string '6.0' "$plist_file" + plutil -insert CFBundleName -string "$lib_name" "$plist_file" + plutil -insert CFBundlePackageType -string FMWK "$plist_file" + plutil -insert CFBundleShortVersionString -string "$version_string" "$plist_file" + plutil -insert CFBundleVersion -string "$version_string" "$plist_file" + plutil -insert MinimumOSVersion -string "$DEPLOYMENT_TARGET" "$plist_file" + plutil -insert CFBundleSupportedPlatforms -array "$plist_file" + plutil -insert CFBundleSupportedPlatforms -string iPhoneOS -append "$plist_file" + + done + popd > /dev/null +done + +pushd . > /dev/null +mkdir -p iOS-xcframework +cd iOS-arm64 +for framework_arm in *.framework; do + xcodebuild -create-xcframework \ + -framework "$framework_arm" \ + -framework "../iOS-simulator-universal/$framework_arm" \ + -output "../iOS-xcframework/${framework_arm%.framework}.xcframework" +done +popd > /dev/null + From 53affedbd7eaf940c59bd46d084b8eeaaae49833 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 30 Apr 2024 11:44:43 +0200 Subject: [PATCH 031/140] Don't call `GameHost.Collect()` twice We already trim memory on a `SDL_EVENT_LOW_MEMORY`. --- osu.Framework.Android/AndroidGameActivity.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Framework.Android/AndroidGameActivity.cs b/osu.Framework.Android/AndroidGameActivity.cs index fe9d25c74e..ff0e51233d 100644 --- a/osu.Framework.Android/AndroidGameActivity.cs +++ b/osu.Framework.Android/AndroidGameActivity.cs @@ -80,12 +80,6 @@ protected override void OnCreate(Bundle? savedInstanceState) } } - public override void OnTrimMemory(TrimMemory level) - { - base.OnTrimMemory(level); - host?.Collect(); - } - protected override void OnStop() { base.OnStop(); From 673fa11f5001510db108435b2327b5356044e8c6 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Tue, 30 Apr 2024 11:45:41 +0200 Subject: [PATCH 032/140] Don't store the host in `AndroidGameActivity` --- osu.Framework.Android/AndroidGameActivity.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Framework.Android/AndroidGameActivity.cs b/osu.Framework.Android/AndroidGameActivity.cs index ff0e51233d..1564bb24c1 100644 --- a/osu.Framework.Android/AndroidGameActivity.cs +++ b/osu.Framework.Android/AndroidGameActivity.cs @@ -11,7 +11,6 @@ using ManagedBass; using Org.Libsdl.App; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Platform; using Debug = System.Diagnostics.Debug; namespace osu.Framework.Android @@ -34,8 +33,6 @@ public abstract class AndroidGameActivity : SDLActivity internal static AndroidGameSurface Surface => (AndroidGameSurface)MSurface!; - private GameHost? host; - protected abstract Game CreateGame(); protected override string[] GetLibraries() => new string[] { "SDL3" }; @@ -44,7 +41,7 @@ public abstract class AndroidGameActivity : SDLActivity protected override IRunnable CreateSDLMainRunnable() => new Runnable(() => { - host = new AndroidGameHost(this); + var host = new AndroidGameHost(this); host.AllowScreenSuspension.Result.BindValueChanged(allow => { RunOnUiThread(() => From 00646c697c81620f4117c579b11660c363b8df55 Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Sat, 11 May 2024 23:20:59 +0200 Subject: [PATCH 033/140] Set sysroot and use rpath for iOS ffmpeg --- osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh index 957d04142b..6b262aa5dd 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh @@ -29,6 +29,7 @@ cpu='' cross_arch='' cc='' as='' +sysroot='' cflags='' case $arch in @@ -37,6 +38,7 @@ case $arch in cross_arch='arm64' cc='xcrun -sdk iphoneos clang' as="$GAS_PREPROCESSOR -arch arm64 -- $cc" + sysroot=$(xcrun -sdk iphoneos --show-sdk-path) cflags="-mios-version-min=$DEPLOYMENT_TARGET" ;; @@ -45,6 +47,7 @@ case $arch in cross_arch='arm64' cc='xcrun -sdk iphonesimulator clang' as="$GAS_PREPROCESSOR -arch arm64 -- $cc" + sysroot=$(xcrun -sdk iphonesimulator --show-sdk-path) cflags="-mios-simulator-version-min=$DEPLOYMENT_TARGET" ;; @@ -53,6 +56,7 @@ case $arch in cross_arch='x86_64' cc='xcrun -sdk iphonesimulator clang' as="$GAS_PREPROCESSOR -- $cc" + sysroot=$(xcrun -sdk iphonesimulator --show-sdk-path) cflags="-mios-simulator-version-min=$DEPLOYMENT_TARGET" ;; esac @@ -70,8 +74,10 @@ FFMPEG_FLAGS+=( --arch=$cross_arch --cc="$cc" --as="$as" - --extra-cflags="-arch $cross_arch $cflags" - --extra-ldflags="-arch $cross_arch $cflags" + --extra-cflags="-isysroot $sysroot -arch $cross_arch $cflags" + --extra-ldflags="-isysroot $sysroot -arch $cross_arch $cflags" + + --install-name-dir='@rpath' ) pushd . > /dev/null From ff9994370428883bda94a9ca98c81ba46a4bbd2f Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Tue, 14 May 2024 11:44:30 +0200 Subject: [PATCH 034/140] Move configure step to build_ffmpeg() prep and build are only separated in order to facilitate patching, and patching is easier before ./configure is run --- osu.Framework.NativeLibs/scripts/ffmpeg/common.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh index efd7c954b6..522b177112 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/common.sh @@ -51,14 +51,14 @@ function prep_ffmpeg() { echo "-> $build_dir already exists, skipping unpacking." fi - echo "-> Configuring..." cd "$build_dir" - ./configure "${FFMPEG_FLAGS[@]}" } function build_ffmpeg() { - echo "-> Building using $CORES threads..." + echo "-> Configuring..." + ./configure "${FFMPEG_FLAGS[@]}" + echo "-> Building using $CORES threads..." make -j$CORES make install-libs } From f55ee6cae43324dd9bdab8c4a56a0d268f5e8d7b Mon Sep 17 00:00:00 2001 From: FreezyLemon Date: Tue, 14 May 2024 11:31:17 +0200 Subject: [PATCH 035/140] Patch iOS install_name --- .../scripts/ffmpeg/build-iOS.sh | 4 ++++ .../iOS-set-install-name-for-xcframework.patch | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 osu.Framework.NativeLibs/scripts/ffmpeg/iOS-set-install-name-for-xcframework.patch diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh b/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh index 6b262aa5dd..7b5daeb0ea 100755 --- a/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/build-iOS.sh @@ -82,6 +82,10 @@ FFMPEG_FLAGS+=( pushd . > /dev/null prep_ffmpeg "iOS-$arch" +# Change the `-install_name` from +# "/libavcodec.dylib.61" to "/libavcodec.framework/libavcodec". +# This is required for framework bundles and xcframeworks to load correctly. +patch -p1 < "$SCRIPT_PATH/iOS-set-install-name-for-xcframework.patch" build_ffmpeg popd > /dev/null diff --git a/osu.Framework.NativeLibs/scripts/ffmpeg/iOS-set-install-name-for-xcframework.patch b/osu.Framework.NativeLibs/scripts/ffmpeg/iOS-set-install-name-for-xcframework.patch new file mode 100644 index 0000000000..9a34485807 --- /dev/null +++ b/osu.Framework.NativeLibs/scripts/ffmpeg/iOS-set-install-name-for-xcframework.patch @@ -0,0 +1,13 @@ +diff --git a/configure b/configure +index 4f5353f84b..dfddd13c9d 100755 +--- a/configure ++++ b/configure +@@ -5738,7 +5738,7 @@ case $target_os in + darwin) + enabled ppc && add_asflags -force_cpusubtype_ALL + install_name_dir_default='$(SHLIBDIR)' +- SHFLAGS='-dynamiclib -Wl,-single_module -Wl,-install_name,$(INSTALL_NAME_DIR)/$(SLIBNAME_WITH_MAJOR),-current_version,$(LIBVERSION),-compatibility_version,$(LIBMAJOR)' ++ SHFLAGS='-dynamiclib -Wl,-single_module -Wl,-install_name,$(INSTALL_NAME_DIR)/$(SLIBPREF)$(FULLNAME).framework/$(SLIBPREF)$(FULLNAME),-current_version,$(LIBVERSION),-compatibility_version,$(LIBMAJOR)' + enabled x86_32 && append SHFLAGS -Wl,-read_only_relocs,suppress + strip="${strip} -x" + add_ldflags -Wl,-dynamic,-search_paths_first From 8335d73b7757650372a08174c7639bdc32d8de4a Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 20 May 2024 16:37:59 +0200 Subject: [PATCH 036/140] Don't pass in `InputState` to inner methods It's still there in `public` `IsPressed` for the breaking change. --- .../Input/Bindings/KeyCombination.cs | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 8778604127..72c659fb39 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -102,10 +102,10 @@ public bool IsPressed(KeyCombination pressedKeys, InputState inputState, KeyComb if (Keys == pressedKeys.Keys) // Fast test for reference equality of underlying array return true; - return ContainsAll(Keys, pressedKeys.Keys, inputState, matchingMode); + return ContainsAll(Keys, pressedKeys.Keys, matchingMode); } - private static InputKey? getVirtualKey(InputKey key, InputState inputState) + private static InputKey? getVirtualKey(InputKey key) { switch (key) { @@ -134,11 +134,10 @@ public bool IsPressed(KeyCombination pressedKeys, InputState inputState, KeyComb /// /// The candidate key binding to match against. /// The keys which have been pressed by a user. - /// The current input state. /// The matching mode to be used when checking. /// Whether this is a match. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool ContainsAll(ImmutableArray candidateKeyBinding, ImmutableArray pressedPhysicalKeys, InputState inputState, KeyCombinationMatchingMode matchingMode) + internal static bool ContainsAll(ImmutableArray candidateKeyBinding, ImmutableArray pressedPhysicalKeys, KeyCombinationMatchingMode matchingMode) { Debug.Assert(pressedPhysicalKeys.All(k => k.IsPhysical())); @@ -147,7 +146,7 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I // the behaviour of excess keys). foreach (var key in candidateKeyBinding) { - if (!IsPressed(pressedPhysicalKeys, key, inputState)) + if (!IsPressed(pressedPhysicalKeys, key)) return false; } @@ -157,7 +156,7 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I foreach (var key in pressedPhysicalKeys) { // in exact matching mode, every pressed key needs to be in the candidate. - if (!KeyBindingContains(candidateKeyBinding, key, inputState)) + if (!KeyBindingContains(candidateKeyBinding, key)) return false; } @@ -167,7 +166,7 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I foreach (var key in pressedPhysicalKeys) { // in modifiers match mode, the same check applies as exact but only for modifier keys. - if (IsModifierKey(key) && !KeyBindingContains(candidateKeyBinding, key, inputState)) + if (IsModifierKey(key) && !KeyBindingContains(candidateKeyBinding, key)) return false; } @@ -186,12 +185,11 @@ internal static bool ContainsAll(ImmutableArray candidateKeyBinding, I /// /// The candidate key binding to match against. /// The physical key that has been pressed. - /// The current input state. /// Whether this is a match. - internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, InputKey physicalKey, InputState inputState) + internal static bool KeyBindingContains(ImmutableArray candidateKeyBinding, InputKey physicalKey) { return candidateKeyBinding.Contains(physicalKey) || - (getVirtualKey(physicalKey, inputState) is InputKey vKey && candidateKeyBinding.Contains(vKey)); + (getVirtualKey(physicalKey) is InputKey vKey && candidateKeyBinding.Contains(vKey)); } /// @@ -199,15 +197,14 @@ internal static bool KeyBindingContains(ImmutableArray candidateKeyBin /// /// The currently pressed keys to match against. /// The candidate key to check. - /// The current input state. /// Whether this is a match. - internal static bool IsPressed(ImmutableArray pressedPhysicalKeys, InputKey candidateKey, InputState inputState) + internal static bool IsPressed(ImmutableArray pressedPhysicalKeys, InputKey candidateKey) { if (candidateKey.IsPhysical()) return pressedPhysicalKeys.Contains(candidateKey); Debug.Assert(candidateKey.IsVirtual()); - return pressedPhysicalKeys.Any(k => getVirtualKey(k, inputState) == candidateKey); + return pressedPhysicalKeys.Any(k => getVirtualKey(k) == candidateKey); } public bool Equals(KeyCombination other) => Keys.SequenceEqual(other.Keys); From dbfed584082560fc39c6b9914e792bd381e570b9 Mon Sep 17 00:00:00 2001 From: Susko3 Date: Mon, 20 May 2024 17:35:14 +0200 Subject: [PATCH 037/140] Fix tests --- osu.Framework.Tests/Input/KeyCombinationTest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Framework.Tests/Input/KeyCombinationTest.cs b/osu.Framework.Tests/Input/KeyCombinationTest.cs index 40099a9976..efaadf57c1 100644 --- a/osu.Framework.Tests/Input/KeyCombinationTest.cs +++ b/osu.Framework.Tests/Input/KeyCombinationTest.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Input.Bindings; -using osu.Framework.Input.States; namespace osu.Framework.Tests.Input { @@ -31,7 +30,7 @@ public class KeyCombinationTest [TestCaseSource(nameof(key_combination_display_test_cases))] public void TestLeftRightModifierHandling(KeyCombination candidate, KeyCombination pressed, KeyCombinationMatchingMode matchingMode, bool shouldContain) - => Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, new InputState(), matchingMode)); + => Assert.AreEqual(shouldContain, KeyCombination.ContainsAll(candidate.Keys, pressed.Keys, matchingMode)); [Test] public void TestCreationNoDuplicates() From 0417ca706f830183dd12efb2bd0a8444d1a4fb56 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 16:09:02 +0900 Subject: [PATCH 038/140] Add SDL2 package reference --- osu.Framework/osu.Framework.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 56969f5609..bdd3d74dfd 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -39,6 +39,7 @@ + From 7ee5f6f4d5c2982b000cc0f81e11903505c8dae2 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 16:30:32 +0900 Subject: [PATCH 039/140] Renamespace classes to SDL3 Using `using SDL.SDL3` to fix namespace conflicts. --- .../Visual/Platform/TestSceneBorderless.cs | 1 + .../Visual/Platform/TestSceneFullscreen.cs | 1 + .../Visual/Platform/TestSceneWindowed.cs | 1 + .../Visual/Platform/WindowDisplaysPreview.cs | 1 + .../Handlers/Joystick/JoystickHandler.cs | 1 + .../Handlers/Keyboard/KeyboardHandler.cs | 1 + .../Input/Handlers/Mouse/MouseHandler.cs | 1 + .../Input/Handlers/Touch/TouchHandler.cs | 1 + osu.Framework/Input/SDL3WindowTextInput.cs | 2 +- osu.Framework/Input/UserInputManager.cs | 1 + osu.Framework/Platform/Linux/LinuxGameHost.cs | 5 +- .../LinuxReadableKeyCombinationProvider.cs | 2 +- .../MacOSReadableKeyCombinationProvider.cs | 2 +- osu.Framework/Platform/MacOS/MacOSWindow.cs | 1 + .../Platform/{SDL => SDL3}/SDL3Clipboard.cs | 20 ++-- .../{SDL => SDL3}/SDL3ControllerBindings.cs | 7 +- .../Platform/{ => SDL3}/SDL3DesktopWindow.cs | 9 +- .../Platform/{SDL => SDL3}/SDL3Extensions.cs | 12 +- .../{SDL => SDL3}/SDL3GraphicsSurface.cs | 62 +++++----- .../Platform/{ => SDL3}/SDL3MobileWindow.cs | 5 +- .../SDL3ReadableKeyCombinationProvider.cs | 8 +- .../Platform/{ => SDL3}/SDL3Window.cs | 106 +++++++++--------- .../Platform/{ => SDL3}/SDL3Window_Input.cs | 48 ++++---- .../{ => SDL3}/SDL3Window_Windowing.cs | 84 +++++++------- osu.Framework/Platform/SDL3GameHost.cs | 2 +- .../WindowsReadableKeyCombinationProvider.cs | 2 +- .../Platform/Windows/WindowsWindow.cs | 11 +- 27 files changed, 206 insertions(+), 191 deletions(-) rename osu.Framework/Platform/{SDL => SDL3}/SDL3Clipboard.cs (90%) rename osu.Framework/Platform/{SDL => SDL3}/SDL3ControllerBindings.cs (90%) rename osu.Framework/Platform/{ => SDL3}/SDL3DesktopWindow.cs (79%) rename osu.Framework/Platform/{SDL => SDL3}/SDL3Extensions.cs (98%) rename osu.Framework/Platform/{SDL => SDL3}/SDL3GraphicsSurface.cs (70%) rename osu.Framework/Platform/{ => SDL3}/SDL3MobileWindow.cs (82%) rename osu.Framework/Platform/{SDL => SDL3}/SDL3ReadableKeyCombinationProvider.cs (96%) rename osu.Framework/Platform/{ => SDL3}/SDL3Window.cs (82%) rename osu.Framework/Platform/{ => SDL3}/SDL3Window_Input.cs (93%) rename osu.Framework/Platform/{ => SDL3}/SDL3Window_Windowing.cs (90%) diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs b/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs index 7c26a30830..63c6a53ebc 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; namespace osu.Framework.Tests.Visual.Platform { diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs b/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs index e1907944b1..f90c3f30f6 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs @@ -19,6 +19,7 @@ using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osuTK; using WindowState = osu.Framework.Platform.WindowState; diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs b/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs index 5015a1e4ff..b192f832e6 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osuTK; using WindowState = osu.Framework.Platform.WindowState; diff --git a/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs b/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs index abb03eae3d..86a772258c 100644 --- a/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs +++ b/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osuTK; using osuTK.Graphics; diff --git a/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs b/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs index 0a0ca30b13..55f92ce66e 100644 --- a/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs +++ b/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Input.StateChanges; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; namespace osu.Framework.Input.Handlers.Joystick diff --git a/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs b/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs index afe149b698..31bb8b7836 100644 --- a/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs +++ b/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs @@ -3,6 +3,7 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; using TKKey = osuTK.Input.Key; diff --git a/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs b/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs index 7150404e0d..713d52fcfe 100644 --- a/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs +++ b/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.StateChanges; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; using osuTK; using osuTK.Input; diff --git a/osu.Framework/Input/Handlers/Touch/TouchHandler.cs b/osu.Framework/Input/Handlers/Touch/TouchHandler.cs index 372f802729..61eb2dff19 100644 --- a/osu.Framework/Input/Handlers/Touch/TouchHandler.cs +++ b/osu.Framework/Input/Handlers/Touch/TouchHandler.cs @@ -3,6 +3,7 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; namespace osu.Framework.Input.Handlers.Touch diff --git a/osu.Framework/Input/SDL3WindowTextInput.cs b/osu.Framework/Input/SDL3WindowTextInput.cs index 641724f114..71b450a86f 100644 --- a/osu.Framework/Input/SDL3WindowTextInput.cs +++ b/osu.Framework/Input/SDL3WindowTextInput.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Primitives; -using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; namespace osu.Framework.Input { diff --git a/osu.Framework/Input/UserInputManager.cs b/osu.Framework/Input/UserInputManager.cs index a066de1d25..7671e41f64 100644 --- a/osu.Framework/Input/UserInputManager.cs +++ b/osu.Framework/Input/UserInputManager.cs @@ -10,6 +10,7 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges.Events; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using osuTK; using osuTK.Input; using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; diff --git a/osu.Framework/Platform/Linux/LinuxGameHost.cs b/osu.Framework/Platform/Linux/LinuxGameHost.cs index 4602407709..fb08932dd6 100644 --- a/osu.Framework/Platform/Linux/LinuxGameHost.cs +++ b/osu.Framework/Platform/Linux/LinuxGameHost.cs @@ -6,7 +6,8 @@ using osu.Framework.Input; using osu.Framework.Input.Handlers; using osu.Framework.Input.Handlers.Mouse; -using SDL; +using osu.Framework.Platform.SDL3; +using static SDL.SDL3; namespace osu.Framework.Platform.Linux { @@ -29,7 +30,7 @@ internal LinuxGameHost(string gameName, HostOptions? options) protected override void SetupForRun() { - SDL3.SDL_SetHint(SDL3.SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, BypassCompositor ? "1"u8 : "0"u8); + SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, BypassCompositor ? "1"u8 : "0"u8); base.SetupForRun(); } diff --git a/osu.Framework/Platform/Linux/LinuxReadableKeyCombinationProvider.cs b/osu.Framework/Platform/Linux/LinuxReadableKeyCombinationProvider.cs index 36cb71d42c..cc1103ed8e 100644 --- a/osu.Framework/Platform/Linux/LinuxReadableKeyCombinationProvider.cs +++ b/osu.Framework/Platform/Linux/LinuxReadableKeyCombinationProvider.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Input.Bindings; -using osu.Framework.Platform.SDL; +using osu.Framework.Platform.SDL3; using SDL; namespace osu.Framework.Platform.Linux diff --git a/osu.Framework/Platform/MacOS/MacOSReadableKeyCombinationProvider.cs b/osu.Framework/Platform/MacOS/MacOSReadableKeyCombinationProvider.cs index de321c54c4..66cc4083a1 100644 --- a/osu.Framework/Platform/MacOS/MacOSReadableKeyCombinationProvider.cs +++ b/osu.Framework/Platform/MacOS/MacOSReadableKeyCombinationProvider.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Input.Bindings; -using osu.Framework.Platform.SDL; +using osu.Framework.Platform.SDL3; using SDL; namespace osu.Framework.Platform.MacOS diff --git a/osu.Framework/Platform/MacOS/MacOSWindow.cs b/osu.Framework/Platform/MacOS/MacOSWindow.cs index 7d6ebd9ec4..98b140d34b 100644 --- a/osu.Framework/Platform/MacOS/MacOSWindow.cs +++ b/osu.Framework/Platform/MacOS/MacOSWindow.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Platform.MacOS.Native; +using osu.Framework.Platform.SDL3; using osuTK; namespace osu.Framework.Platform.MacOS diff --git a/osu.Framework/Platform/SDL/SDL3Clipboard.cs b/osu.Framework/Platform/SDL3/SDL3Clipboard.cs similarity index 90% rename from osu.Framework/Platform/SDL/SDL3Clipboard.cs rename to osu.Framework/Platform/SDL3/SDL3Clipboard.cs index 786da27fd5..b3d1e42733 100644 --- a/osu.Framework/Platform/SDL/SDL3Clipboard.cs +++ b/osu.Framework/Platform/SDL3/SDL3Clipboard.cs @@ -16,7 +16,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; -namespace osu.Framework.Platform.SDL +namespace osu.Framework.Platform.SDL3 { public class SDL3Clipboard : Clipboard { @@ -40,9 +40,9 @@ public SDL3Clipboard(IImageFormat imageFormat) // SDL cannot differentiate between string.Empty and no text (eg. empty clipboard or an image) // doesn't matter as text editors don't really allow copying empty strings. // assume that empty text means no text. - public override string? GetText() => SDL3.SDL_HasClipboardText() == SDL_bool.SDL_TRUE ? SDL3.SDL_GetClipboardText() : null; + public override string? GetText() => SDL.SDL3.SDL_HasClipboardText() == SDL_bool.SDL_TRUE ? SDL.SDL3.SDL_GetClipboardText() : null; - public override void SetText(string text) => SDL3.SDL_SetClipboardText(text); + public override void SetText(string text) => SDL.SDL3.SDL_SetClipboardText(text); public override Image? GetImage() { @@ -85,18 +85,18 @@ public override bool SetImage(Image image) private static unsafe bool tryGetData(string mimeType, SpanDecoder decoder, out T? data) { - if (SDL3.SDL_HasClipboardData(mimeType) == SDL_bool.SDL_FALSE) + if (SDL.SDL3.SDL_HasClipboardData(mimeType) == SDL_bool.SDL_FALSE) { data = default; return false; } UIntPtr nativeSize; - IntPtr pointer = SDL3.SDL_GetClipboardData(mimeType, &nativeSize); + IntPtr pointer = SDL.SDL3.SDL_GetClipboardData(mimeType, &nativeSize); if (pointer == IntPtr.Zero) { - Logger.Log($"Failed to get SDL clipboard data for {mimeType}. SDL error: {SDL3.SDL_GetError()}"); + Logger.Log($"Failed to get SDL clipboard data for {mimeType}. SDL error: {SDL.SDL3.SDL_GetError()}"); data = default; return false; } @@ -115,7 +115,7 @@ private static unsafe bool tryGetData(string mimeType, SpanDecoder decoder } finally { - SDL3.SDL_free(pointer); + SDL.SDL3.SDL_free(pointer); } } @@ -127,12 +127,12 @@ private static unsafe bool trySetData(string mimeType, Func // TODO: support multiple mime types in a single callback fixed (byte* ptr = Encoding.UTF8.GetBytes(mimeType + '\0')) { - int ret = SDL3.SDL_SetClipboardData(&dataCallback, &cleanupCallback, objectHandle.Handle, &ptr, 1); + int ret = SDL.SDL3.SDL_SetClipboardData(&dataCallback, &cleanupCallback, objectHandle.Handle, &ptr, 1); if (ret < 0) { objectHandle.Dispose(); - Logger.Log($"Failed to set clipboard data callback. SDL error: {SDL3.SDL_GetError()}"); + Logger.Log($"Failed to set clipboard data callback. SDL error: {SDL.SDL3.SDL_GetError()}"); } return ret == 0; @@ -144,7 +144,7 @@ private static unsafe IntPtr dataCallback(IntPtr userdata, byte* mimeType, UIntP { using var objectHandle = new ObjectHandle(userdata); - if (!objectHandle.GetTarget(out var context) || context.MimeType != SDL3.PtrToStringUTF8(mimeType)) + if (!objectHandle.GetTarget(out var context) || context.MimeType != SDL.SDL3.PtrToStringUTF8(mimeType)) { *length = 0; return IntPtr.Zero; diff --git a/osu.Framework/Platform/SDL/SDL3ControllerBindings.cs b/osu.Framework/Platform/SDL3/SDL3ControllerBindings.cs similarity index 90% rename from osu.Framework/Platform/SDL/SDL3ControllerBindings.cs rename to osu.Framework/Platform/SDL3/SDL3ControllerBindings.cs index 15bbdb55d4..cf7c1a237d 100644 --- a/osu.Framework/Platform/SDL/SDL3ControllerBindings.cs +++ b/osu.Framework/Platform/SDL3/SDL3ControllerBindings.cs @@ -5,8 +5,9 @@ using System; using SDL; +using static SDL.SDL3; -namespace osu.Framework.Platform.SDL +namespace osu.Framework.Platform.SDL3 { /// /// Maintain a copy of the SDL-provided bindings for the given controller. @@ -18,7 +19,7 @@ internal unsafe class SDL3ControllerBindings public readonly SDL_Gamepad* GamepadHandle; /// - /// Bindings returned from . + /// Bindings returned from . /// Empty if the joystick does not have a corresponding GamepadHandle. /// public SDL_GamepadBinding[] Bindings; @@ -39,7 +40,7 @@ public void PopulateBindings() return; } - using var bindings = SDL3.SDL_GetGamepadBindings(GamepadHandle); + using var bindings = SDL.SDL3.SDL_GetGamepadBindings(GamepadHandle); if (bindings == null) { diff --git a/osu.Framework/Platform/SDL3DesktopWindow.cs b/osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs similarity index 79% rename from osu.Framework/Platform/SDL3DesktopWindow.cs rename to osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs index 316fd8e0e1..14fe14c0fe 100644 --- a/osu.Framework/Platform/SDL3DesktopWindow.cs +++ b/osu.Framework/Platform/SDL3/SDL3DesktopWindow.cs @@ -2,8 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using SDL; +using static SDL.SDL3; -namespace osu.Framework.Platform +namespace osu.Framework.Platform.SDL3 { internal class SDL3DesktopWindow : SDL3Window { @@ -17,9 +18,9 @@ protected override unsafe void UpdateWindowStateAndSize(WindowState state, Displ // this reset is required even on changing from one fullscreen resolution to another. // if it is not included, the GL context will not get the correct size. // this is mentioned by multiple sources as an SDL issue, which seems to resolve by similar means (see https://discourse.libsdl.org/t/sdl-setwindowsize-does-not-work-in-fullscreen/20711/4). - SDL3.SDL_SetWindowBordered(SDLWindowHandle, SDL_bool.SDL_TRUE); - SDL3.SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_FALSE); - SDL3.SDL_RestoreWindow(SDLWindowHandle); + SDL_SetWindowBordered(SDLWindowHandle, SDL_bool.SDL_TRUE); + SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_FALSE); + SDL_RestoreWindow(SDLWindowHandle); base.UpdateWindowStateAndSize(state, display, displayMode); } diff --git a/osu.Framework/Platform/SDL/SDL3Extensions.cs b/osu.Framework/Platform/SDL3/SDL3Extensions.cs similarity index 98% rename from osu.Framework/Platform/SDL/SDL3Extensions.cs rename to osu.Framework/Platform/SDL3/SDL3Extensions.cs index 1e37f25a1a..de9ed74258 100644 --- a/osu.Framework/Platform/SDL/SDL3Extensions.cs +++ b/osu.Framework/Platform/SDL3/SDL3Extensions.cs @@ -10,7 +10,7 @@ using osuTK.Input; using SDL; -namespace osu.Framework.Platform.SDL +namespace osu.Framework.Platform.SDL3 { public static class SDL3Extensions { @@ -1016,8 +1016,8 @@ public static unsafe DisplayMode ToDisplayMode(this SDL_DisplayMode mode, int di { int bpp; uint unused; - SDL3.SDL_GetMasksForPixelFormatEnum(mode.format, &bpp, &unused, &unused, &unused, &unused); - return new DisplayMode(SDL3.SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex); + SDL.SDL3.SDL_GetMasksForPixelFormatEnum(mode.format, &bpp, &unused, &unused, &unused, &unused); + return new DisplayMode(SDL.SDL3.SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex); } public static string ReadableName(this SDL_LogCategory category) @@ -1096,8 +1096,8 @@ public static string ReadableName(this SDL_LogPriority priority) /// public static string? GetAndClearError() { - string? error = SDL3.SDL_GetError(); - SDL3.SDL_ClearError(); + string? error = SDL.SDL3.SDL_GetError(); + SDL.SDL3.SDL_ClearError(); return error; } @@ -1109,7 +1109,7 @@ public static string ReadableName(this SDL_LogPriority priority) /// public static bool TryGetTouchName(this SDL_TouchFingerEvent e, [NotNullWhen(true)] out string? name) { - name = SDL3.SDL_GetTouchDeviceName(e.touchID); + name = SDL.SDL3.SDL_GetTouchDeviceName(e.touchID); return name != null; } } diff --git a/osu.Framework/Platform/SDL/SDL3GraphicsSurface.cs b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs similarity index 70% rename from osu.Framework/Platform/SDL/SDL3GraphicsSurface.cs rename to osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs index 6651526ef8..8ebf4fc0de 100644 --- a/osu.Framework/Platform/SDL/SDL3GraphicsSurface.cs +++ b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs @@ -12,7 +12,7 @@ using osuTK.Graphics.ES30; using SDL; -namespace osu.Framework.Platform.SDL +namespace osu.Framework.Platform.SDL3 { internal unsafe class SDL3GraphicsSurface : IGraphicsSurface, IOpenGLGraphicsSurface, IMetalGraphicsSurface, ILinuxGraphicsSurface, IAndroidGraphicsSurface { @@ -32,12 +32,12 @@ public SDL3GraphicsSurface(SDL3Window window, GraphicsSurfaceType surfaceType) switch (surfaceType) { case GraphicsSurfaceType.OpenGL: - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_RED_SIZE, 8); - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_GREEN_SIZE, 8); - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_BLUE_SIZE, 8); - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ACCUM_ALPHA_SIZE, 0); - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DEPTH_SIZE, 16); - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STENCIL_SIZE, 8); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_RED_SIZE, 8); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_GREEN_SIZE, 8); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_BLUE_SIZE, 8); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ACCUM_ALPHA_SIZE, 0); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DEPTH_SIZE, 16); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STENCIL_SIZE, 8); break; case GraphicsSurfaceType.Vulkan: @@ -59,7 +59,7 @@ public void Initialise() public Size GetDrawableSize() { int width, height; - SDL3.SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height); + SDL.SDL3.SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height); return new Size(width, height); } @@ -69,27 +69,27 @@ private void initialiseOpenGL() { if (RuntimeInfo.IsMobile) { - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_ES); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_ES); // Minimum OpenGL version for ES profile: - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 0); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 0); } else { - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_CORE); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_CORE); // Minimum OpenGL version for core profile: - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 2); } - context = SDL3.SDL_GL_CreateContext(window.SDLWindowHandle); + context = SDL.SDL3.SDL_GL_CreateContext(window.SDLWindowHandle); if (context == IntPtr.Zero) - throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL3.SDL_GetError()})"); + throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL.SDL3.SDL_GetError()})"); - SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, context); + SDL.SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, context); loadBindings(); } @@ -135,15 +135,15 @@ private void loadEntryPoints(GraphicsBindingsBase bindings) private IntPtr getProcAddress(string symbol) { const SDL_LogCategory error_category = SDL_LogCategory.SDL_LOG_CATEGORY_ERROR; - SDL_LogPriority oldPriority = SDL3.SDL_LogGetPriority(error_category); + SDL_LogPriority oldPriority = SDL.SDL3.SDL_LogGetPriority(error_category); // Prevent logging calls to SDL_GL_GetProcAddress() that fail on systems which don't have the requested symbol (typically macOS). - SDL3.SDL_LogSetPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO); + SDL.SDL3.SDL_LogSetPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO); - IntPtr ret = SDL3.SDL_GL_GetProcAddress(symbol); + IntPtr ret = SDL.SDL3.SDL_GL_GetProcAddress(symbol); // Reset the logging behaviour. - SDL3.SDL_LogSetPriority(error_category, oldPriority); + SDL.SDL3.SDL_LogSetPriority(error_category, oldPriority); return ret; } @@ -180,34 +180,34 @@ bool IOpenGLGraphicsSurface.VerticalSync return verticalSync.Value; int interval; - SDL3.SDL_GL_GetSwapInterval(&interval); + SDL.SDL3.SDL_GL_GetSwapInterval(&interval); return (verticalSync = interval != 0).Value; } set { if (RuntimeInfo.IsDesktop) { - SDL3.SDL_GL_SetSwapInterval(value ? 1 : 0); + SDL.SDL3.SDL_GL_SetSwapInterval(value ? 1 : 0); verticalSync = value; } } } IntPtr IOpenGLGraphicsSurface.WindowContext => context; - IntPtr IOpenGLGraphicsSurface.CurrentContext => SDL3.SDL_GL_GetCurrentContext(); + IntPtr IOpenGLGraphicsSurface.CurrentContext => SDL.SDL3.SDL_GL_GetCurrentContext(); - void IOpenGLGraphicsSurface.SwapBuffers() => SDL3.SDL_GL_SwapWindow(window.SDLWindowHandle); - void IOpenGLGraphicsSurface.CreateContext() => SDL3.SDL_GL_CreateContext(window.SDLWindowHandle); - void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL3.SDL_GL_DeleteContext(context); - void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, context); - void IOpenGLGraphicsSurface.ClearCurrent() => SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, IntPtr.Zero); + void IOpenGLGraphicsSurface.SwapBuffers() => SDL.SDL3.SDL_GL_SwapWindow(window.SDLWindowHandle); + void IOpenGLGraphicsSurface.CreateContext() => SDL.SDL3.SDL_GL_CreateContext(window.SDLWindowHandle); + void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL.SDL3.SDL_GL_DeleteContext(context); + void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL.SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, context); + void IOpenGLGraphicsSurface.ClearCurrent() => SDL.SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, IntPtr.Zero); IntPtr IOpenGLGraphicsSurface.GetProcAddress(string symbol) => getProcAddress(symbol); #endregion #region Metal-specific implementation - IntPtr IMetalGraphicsSurface.CreateMetalView() => SDL3.SDL_Metal_CreateView(window.SDLWindowHandle); + IntPtr IMetalGraphicsSurface.CreateMetalView() => SDL.SDL3.SDL_Metal_CreateView(window.SDLWindowHandle); #endregion @@ -223,7 +223,7 @@ bool IOpenGLGraphicsSurface.VerticalSync #region Android-specific implementation [SupportedOSPlatform("android")] - IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL3.SDL_AndroidGetJNIEnv(); + IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL.SDL3.SDL_AndroidGetJNIEnv(); [SupportedOSPlatform("android")] IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle; diff --git a/osu.Framework/Platform/SDL3MobileWindow.cs b/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs similarity index 82% rename from osu.Framework/Platform/SDL3MobileWindow.cs rename to osu.Framework/Platform/SDL3/SDL3MobileWindow.cs index cd16015e5f..ba57f6ab79 100644 --- a/osu.Framework/Platform/SDL3MobileWindow.cs +++ b/osu.Framework/Platform/SDL3/SDL3MobileWindow.cs @@ -2,8 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using SDL; +using static SDL.SDL3; -namespace osu.Framework.Platform +namespace osu.Framework.Platform.SDL3 { internal class SDL3MobileWindow : SDL3Window { @@ -15,7 +16,7 @@ public SDL3MobileWindow(GraphicsSurfaceType surfaceType, string appName) protected override unsafe void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode) { // This sets the status bar to hidden. - SDL3.SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_TRUE); + SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_TRUE); // Don't run base logic at all. Let's keep things simple. } diff --git a/osu.Framework/Platform/SDL/SDL3ReadableKeyCombinationProvider.cs b/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs similarity index 96% rename from osu.Framework/Platform/SDL/SDL3ReadableKeyCombinationProvider.cs rename to osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs index bb2763fced..92ecc06203 100644 --- a/osu.Framework/Platform/SDL/SDL3ReadableKeyCombinationProvider.cs +++ b/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs @@ -6,13 +6,13 @@ using osu.Framework.Input.Bindings; using SDL; -namespace osu.Framework.Platform.SDL +namespace osu.Framework.Platform.SDL3 { public class SDL3ReadableKeyCombinationProvider : ReadableKeyCombinationProvider { protected override string GetReadableKey(InputKey key) { - var keycode = SDL3.SDL_GetKeyFromScancode(key.ToScancode()); + var keycode = SDL.SDL3.SDL_GetKeyFromScancode(key.ToScancode()); // early return if unknown. probably because key isn't a keyboard key, or doesn't map to an `SDL_Scancode`. if (keycode == SDL_Keycode.SDLK_UNKNOWN) @@ -24,7 +24,7 @@ protected override string GetReadableKey(InputKey key) if (TryGetNameFromKeycode(keycode, out name)) return name; - name = SDL3.SDL_GetKeyName(keycode); + name = SDL.SDL3.SDL_GetKeyName(keycode); // fall back if SDL couldn't find a name. if (string.IsNullOrEmpty(name)) @@ -32,7 +32,7 @@ protected override string GetReadableKey(InputKey key) // true if SDL_GetKeyName() returned a proper key/scancode name. // see https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/events/SDL_keyboard.c#L1012 - if (((int)keycode & SDL3.SDLK_SCANCODE_MASK) != 0) + if (((int)keycode & SDL.SDL3.SDLK_SCANCODE_MASK) != 0) return name; // SDL_GetKeyName() returned a unicode character that would be produced if that key was pressed. diff --git a/osu.Framework/Platform/SDL3Window.cs b/osu.Framework/Platform/SDL3/SDL3Window.cs similarity index 82% rename from osu.Framework/Platform/SDL3Window.cs rename to osu.Framework/Platform/SDL3/SDL3Window.cs index 1403416e20..4801a9891f 100644 --- a/osu.Framework/Platform/SDL3Window.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window.cs @@ -12,15 +12,15 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ImageExtensions; using osu.Framework.Logging; -using osu.Framework.Platform.SDL; using osu.Framework.Threading; using SDL; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using Image = SixLabors.ImageSharp.Image; using Point = System.Drawing.Point; +using static SDL.SDL3; -namespace osu.Framework.Platform +namespace osu.Framework.Platform.SDL3 { /// /// Default implementation of a window, using SDL for windowing and graphics support. @@ -70,14 +70,14 @@ public string Title set { title = value; - ScheduleCommand(() => SDL3.SDL_SetWindowTitle(SDLWindowHandle, title)); + ScheduleCommand(() => SDL_SetWindowTitle(SDLWindowHandle, title)); } } /// /// Whether the current display server is Wayland. /// - internal bool IsWayland => SDL3.SDL_GetCurrentVideoDriver() == "wayland"; + internal bool IsWayland => SDL_GetCurrentVideoDriver() == "wayland"; /// /// Gets the native window handle as provided by the operating system. @@ -89,30 +89,30 @@ public IntPtr WindowHandle if (SDLWindowHandle == null) return IntPtr.Zero; - var props = SDL3.SDL_GetWindowProperties(SDLWindowHandle); + var props = SDL_GetWindowProperties(SDLWindowHandle); switch (RuntimeInfo.OS) { case RuntimeInfo.Platform.Windows: - return SDL3.SDL_GetProperty(props, SDL3.SDL_PROP_WINDOW_WIN32_HWND_POINTER, IntPtr.Zero); + return SDL_GetProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, IntPtr.Zero); case RuntimeInfo.Platform.Linux: if (IsWayland) - return SDL3.SDL_GetProperty(props, SDL3.SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, IntPtr.Zero); + return SDL_GetProperty(props, SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, IntPtr.Zero); - if (SDL3.SDL_GetCurrentVideoDriver() == "x11") - return new IntPtr(SDL3.SDL_GetNumberProperty(props, SDL3.SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)); + if (SDL_GetCurrentVideoDriver() == "x11") + return new IntPtr(SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)); return IntPtr.Zero; case RuntimeInfo.Platform.macOS: - return SDL3.SDL_GetProperty(props, SDL3.SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, IntPtr.Zero); + return SDL_GetProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, IntPtr.Zero); case RuntimeInfo.Platform.iOS: - return SDL3.SDL_GetProperty(props, SDL3.SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, IntPtr.Zero); + return SDL_GetProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, IntPtr.Zero); case RuntimeInfo.Platform.Android: - return SDL3.SDL_GetProperty(props, SDL3.SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, IntPtr.Zero); + return SDL_GetProperty(props, SDL_PROP_WINDOW_ANDROID_WINDOW_POINTER, IntPtr.Zero); default: throw new ArgumentOutOfRangeException(); @@ -128,13 +128,13 @@ public IntPtr DisplayHandle if (SDLWindowHandle == null) return IntPtr.Zero; - var props = SDL3.SDL_GetWindowProperties(SDLWindowHandle); + var props = SDL_GetWindowProperties(SDLWindowHandle); if (IsWayland) - return SDL3.SDL_GetProperty(props, SDL3.SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, IntPtr.Zero); + return SDL_GetProperty(props, SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, IntPtr.Zero); - if (SDL3.SDL_GetCurrentVideoDriver() == "x11") - return SDL3.SDL_GetProperty(props, SDL3.SDL_PROP_WINDOW_X11_DISPLAY_POINTER, IntPtr.Zero); + if (SDL_GetCurrentVideoDriver() == "x11") + return SDL_GetProperty(props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, IntPtr.Zero); return IntPtr.Zero; } @@ -143,7 +143,7 @@ public IntPtr DisplayHandle [SupportedOSPlatform("android")] public virtual IntPtr SurfaceHandle => throw new PlatformNotSupportedException(); - public bool CapsLockPressed => SDL3.SDL_GetModState().HasFlagFast(SDL_Keymod.SDL_KMOD_CAPS); + public bool CapsLockPressed => SDL_GetModState().HasFlagFast(SDL_Keymod.SDL_KMOD_CAPS); /// /// Represents a handle to this instance, used for unmanaged callbacks. @@ -154,22 +154,22 @@ protected SDL3Window(GraphicsSurfaceType surfaceType, string appName) { ObjectHandle = new ObjectHandle(this, GCHandleType.Normal); - SDL3.SDL_SetHint(SDL3.SDL_HINT_APP_NAME, appName); + SDL_SetHint(SDL_HINT_APP_NAME, appName); - if (SDL3.SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO | SDL_InitFlags.SDL_INIT_GAMEPAD) < 0) + if (SDL_Init(SDL_InitFlags.SDL_INIT_VIDEO | SDL_InitFlags.SDL_INIT_GAMEPAD) < 0) { - throw new InvalidOperationException($"Failed to initialise SDL: {SDL3.SDL_GetError()}"); + throw new InvalidOperationException($"Failed to initialise SDL: {SDL_GetError()}"); } SDL_Version version; - SDL3.SDL_GetVersion(&version); + SDL_GetVersion(&version); Logger.Log($@"SDL3 Initialized SDL3 Version: {version.major}.{version.minor}.{version.patch} - SDL3 Revision: {SDL3.SDL_GetRevision()}"); + SDL3 Revision: {SDL_GetRevision()}"); - SDL3.SDL_LogSetPriority(SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG); - SDL3.SDL_SetLogOutputFunction(&logOutput, IntPtr.Zero); - SDL3.SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle); + SDL_LogSetPriority(SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG); + SDL_SetLogOutputFunction(&logOutput, IntPtr.Zero); + SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle); graphicsSurface = new SDL3GraphicsSurface(this, surfaceType); @@ -185,7 +185,7 @@ protected SDL3Window(GraphicsSurfaceType surfaceType, string appName) [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] private static void logOutput(IntPtr _, SDL_LogCategory category, SDL_LogPriority priority, byte* messagePtr) { - string? message = SDL3.PtrToStringUTF8(messagePtr); + string? message = PtrToStringUTF8(messagePtr); Logger.Log($@"SDL {category.ReadableName()} log [{priority.ReadableName()}]: {message}"); } @@ -204,21 +204,21 @@ public virtual void Create() flags |= WindowState.ToFlags(); flags |= graphicsSurface.Type.ToFlags(); - SDL3.SDL_SetHint(SDL3.SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8); - SDL3.SDL_SetHint(SDL3.SDL_HINT_IME_SHOW_UI, "1"u8); - SDL3.SDL_SetHint(SDL3.SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8); - SDL3.SDL_SetHint(SDL3.SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8); // disable touch events generating synthetic mouse events on desktop platforms - SDL3.SDL_SetHint(SDL3.SDL_HINT_MOUSE_TOUCH_EVENTS, "0"u8); // disable mouse events generating synthetic touch events on mobile platforms + SDL_SetHint(SDL_HINT_WINDOWS_CLOSE_ON_ALT_F4, "0"u8); + SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"u8); + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"u8); + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"u8); // disable touch events generating synthetic mouse events on desktop platforms + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"u8); // disable mouse events generating synthetic touch events on mobile platforms // we want text input to only be active when SDL3DesktopWindowTextInput is active. // SDL activates it by default on some platforms: https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/video/SDL_video.c#L573-L582 // so we deactivate it on startup. - SDL3.SDL_StopTextInput(); + SDL_StopTextInput(); - SDLWindowHandle = SDL3.SDL_CreateWindow(title, Size.Width, Size.Height, flags); + SDLWindowHandle = SDL_CreateWindow(title, Size.Width, Size.Height, flags); if (SDLWindowHandle == null) - throw new InvalidOperationException($"Failed to create SDL window. SDL Error: {SDL3.SDL_GetError()}"); + throw new InvalidOperationException($"Failed to create SDL window. SDL Error: {SDL_GetError()}"); graphicsSurface.Initialise(); @@ -231,7 +231,7 @@ public virtual void Create() /// public virtual void Run() { - SDL3.SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle); + SDL_AddEventWatch(&eventWatch, ObjectHandle.Handle); RunMainLoop(); } @@ -253,7 +253,7 @@ protected virtual void RunMainLoop() Exited?.Invoke(); Close(); - SDL3.SDL_Quit(); + SDL_Quit(); } /// @@ -365,7 +365,7 @@ public void Close() { if (SDLWindowHandle != null) { - SDL3.SDL_DestroyWindow(SDLWindowHandle); + SDL_DestroyWindow(SDLWindowHandle); SDLWindowHandle = null; } } @@ -373,22 +373,22 @@ public void Close() public void Raise() => ScheduleCommand(() => { - var flags = SDL3.SDL_GetWindowFlags(SDLWindowHandle); + var flags = SDL_GetWindowFlags(SDLWindowHandle); if (flags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MINIMIZED)) - SDL3.SDL_RestoreWindow(SDLWindowHandle); + SDL_RestoreWindow(SDLWindowHandle); - SDL3.SDL_RaiseWindow(SDLWindowHandle); + SDL_RaiseWindow(SDLWindowHandle); }); public void Hide() => ScheduleCommand(() => { - SDL3.SDL_HideWindow(SDLWindowHandle); + SDL_HideWindow(SDLWindowHandle); }); public void Show() => ScheduleCommand(() => { - SDL3.SDL_ShowWindow(SDLWindowHandle); + SDL_ShowWindow(SDLWindowHandle); }); public void Flash(bool flashUntilFocused = false) => ScheduleCommand(() => @@ -399,7 +399,7 @@ public void Flash(bool flashUntilFocused = false) => ScheduleCommand(() => if (!RuntimeInfo.IsDesktop) return; - SDL3.SDL_FlashWindow(SDLWindowHandle, flashUntilFocused + SDL_FlashWindow(SDLWindowHandle, flashUntilFocused ? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED : SDL_FlashOperation.SDL_FLASH_BRIEFLY); }); @@ -409,12 +409,12 @@ public void CancelFlash() => ScheduleCommand(() => if (!RuntimeInfo.IsDesktop) return; - SDL3.SDL_FlashWindow(SDLWindowHandle, SDL_FlashOperation.SDL_FLASH_CANCEL); + SDL_FlashWindow(SDLWindowHandle, SDL_FlashOperation.SDL_FLASH_CANCEL); }); - public void EnableScreenSuspension() => ScheduleCommand(() => SDL3.SDL_EnableScreenSaver()); + public void EnableScreenSuspension() => ScheduleCommand(() => SDL_EnableScreenSaver()); - public void DisableScreenSuspension() => ScheduleCommand(() => SDL3.SDL_DisableScreenSaver()); + public void DisableScreenSuspension() => ScheduleCommand(() => SDL_DisableScreenSaver()); /// /// Attempts to set the window's icon to the specified image. @@ -433,12 +433,12 @@ private void setSDLIcon(Image image) fixed (Rgba32* ptr = pixelSpan) { - var pixelFormat = SDL3.SDL_GetPixelFormatEnumForMasks(32, 0xff, 0xff00, 0xff0000, 0xff000000); - surface = SDL3.SDL_CreateSurfaceFrom(new IntPtr(ptr), imageSize.Width, imageSize.Height, imageSize.Width * 4, pixelFormat); + var pixelFormat = SDL_GetPixelFormatEnumForMasks(32, 0xff, 0xff00, 0xff0000, 0xff000000); + surface = SDL_CreateSurfaceFrom(new IntPtr(ptr), imageSize.Width, imageSize.Height, imageSize.Width * 4, pixelFormat); } - SDL3.SDL_SetWindowIcon(SDLWindowHandle, surface); - SDL3.SDL_DestroySurface(surface); + SDL_SetWindowIcon(SDLWindowHandle, surface); + SDL_DestroySurface(surface); }); } @@ -460,13 +460,13 @@ private void setSDLIcon(Image image) /// private void pollSDLEvents() { - SDL3.SDL_PumpEvents(); + SDL_PumpEvents(); int eventsRead; do { - eventsRead = SDL3.SDL_PeepEvents(events, SDL_eventaction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST); + eventsRead = SDL_PeepEvents(events, SDL_eventaction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST); for (int i = 0; i < eventsRead; i++) HandleEvent(events[i]); } while (eventsRead == events_per_peep); @@ -655,7 +655,7 @@ internal virtual void SetIconFromGroup(IconGroup iconGroup) public void Dispose() { Close(); - SDL3.SDL_Quit(); + SDL_Quit(); ObjectHandle.Dispose(); } diff --git a/osu.Framework/Platform/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs similarity index 93% rename from osu.Framework/Platform/SDL3Window_Input.cs rename to osu.Framework/Platform/SDL3/SDL3Window_Input.cs index 334d96bfef..4e08c3e344 100644 --- a/osu.Framework/Platform/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -12,13 +12,13 @@ using osu.Framework.Input; using osu.Framework.Input.States; using osu.Framework.Logging; -using osu.Framework.Platform.SDL; using osuTK; using osuTK.Input; using SDL; using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; +using static SDL.SDL3; -namespace osu.Framework.Platform +namespace osu.Framework.Platform.SDL3 { internal partial class SDL3Window { @@ -49,7 +49,7 @@ public bool RelativeMouseMode throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}."); relativeMouseMode = value; - ScheduleCommand(() => SDL3.SDL_SetRelativeMouseMode(value ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); + ScheduleCommand(() => SDL_SetRelativeMouseMode(value ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); updateCursorConfinement(); } } @@ -67,7 +67,7 @@ public bool RelativeMouseMode /// internal bool MouseAutoCapture { - set => ScheduleCommand(() => SDL3.SDL_SetHint(SDL3.SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8)); + set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8)); } /// @@ -99,9 +99,9 @@ private void updateCursorVisibility(bool cursorVisible) => ScheduleCommand(() => { if (cursorVisible) - SDL3.SDL_ShowCursor(); + SDL_ShowCursor(); else - SDL3.SDL_HideCursor(); + SDL_HideCursor(); }); /// @@ -111,7 +111,7 @@ private unsafe void updateCursorConfinement() { bool confined = CursorState.HasFlagFast(CursorState.Confined); - ScheduleCommand(() => SDL3.SDL_SetWindowMouseGrab(SDLWindowHandle, confined ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); + ScheduleCommand(() => SDL_SetWindowMouseGrab(SDLWindowHandle, confined ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); // Don't use SDL_SetWindowMouseRect when relative mode is enabled, as relative mode already confines the OS cursor to the window. // This is fine for our use case, as UserInputManager will clamp the mouse position. @@ -120,12 +120,12 @@ private unsafe void updateCursorConfinement() ScheduleCommand(() => { var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect(); - SDL3.SDL_SetWindowMouseRect(SDLWindowHandle, &rect); + SDL_SetWindowMouseRect(SDLWindowHandle, &rect); }); } else { - ScheduleCommand(() => SDL3.SDL_SetWindowMouseRect(SDLWindowHandle, null)); + ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, null)); } } @@ -156,7 +156,7 @@ private void enqueueJoystickButtonInput(JoystickButton button, bool isPressed) private unsafe void pollMouse() { float x, y; - SDLButtonMask globalButtons = (SDLButtonMask)SDL3.SDL_GetGlobalMouseState(&x, &y); + SDLButtonMask globalButtons = (SDLButtonMask)SDL_GetGlobalMouseState(&x, &y); if (previousPolledPoint.X != x || previousPolledPoint.Y != y) { @@ -183,9 +183,9 @@ private unsafe void pollMouse() } } - public virtual void StartTextInput(bool allowIme) => ScheduleCommand(SDL3.SDL_StartTextInput); + public virtual void StartTextInput(bool allowIme) => ScheduleCommand(SDL_StartTextInput); - public void StopTextInput() => ScheduleCommand(SDL3.SDL_StopTextInput); + public void StopTextInput() => ScheduleCommand(SDL_StopTextInput); /// /// Resets internal state of the platform-native IME. @@ -193,14 +193,14 @@ private unsafe void pollMouse() /// public virtual void ResetIme() => ScheduleCommand(() => { - SDL3.SDL_StopTextInput(); - SDL3.SDL_StartTextInput(); + SDL_StopTextInput(); + SDL_StartTextInput(); }); public unsafe void SetTextInputRect(RectangleF rect) => ScheduleCommand(() => { var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect(); - SDL3.SDL_SetTextInputRect(&sdlRect); + SDL_SetTextInputRect(&sdlRect); }); #region SDL Event Handling @@ -291,7 +291,7 @@ private unsafe void handleControllerDeviceEvent(SDL_GamepadDeviceEvent evtCdevic break; case SDL_EventType.SDL_EVENT_GAMEPAD_REMOVED: - SDL3.SDL_CloseGamepad(controllers[evtCdevice.which].GamepadHandle); + SDL_CloseGamepad(controllers[evtCdevice.which].GamepadHandle); controllers.Remove(evtCdevice.which); break; @@ -328,11 +328,11 @@ private unsafe void addJoystick(SDL_JoystickID instanceID) if (controllers.ContainsKey(instanceID)) return; - SDL_Joystick* joystick = SDL3.SDL_OpenJoystick(instanceID); + SDL_Joystick* joystick = SDL_OpenJoystick(instanceID); SDL_Gamepad* controller = null; - if (SDL3.SDL_IsGamepad(instanceID) == SDL_bool.SDL_TRUE) - controller = SDL3.SDL_OpenGamepad(instanceID); + if (SDL_IsGamepad(instanceID) == SDL_bool.SDL_TRUE) + controller = SDL_OpenGamepad(instanceID); controllers[instanceID] = new SDL3ControllerBindings(joystick, controller); } @@ -342,7 +342,7 @@ private unsafe void addJoystick(SDL_JoystickID instanceID) /// private void populateJoysticks() { - using var joysticks = SDL3.SDL_GetJoysticks(); + using var joysticks = SDL_GetJoysticks(); if (joysticks == null) return; @@ -366,7 +366,7 @@ private unsafe void handleJoyDeviceEvent(SDL_JoyDeviceEvent evtJdevice) if (!controllers.ContainsKey(evtJdevice.which)) break; - SDL3.SDL_CloseJoystick(controllers[evtJdevice.which].JoystickHandle); + SDL_CloseJoystick(controllers[evtJdevice.which].JoystickHandle); controllers.Remove(evtJdevice.which); break; } @@ -430,7 +430,7 @@ private void handleMouseWheelEvent(SDL_MouseWheelEvent evtWheel) private void handleMouseButtonEvent(SDL_MouseButtonEvent evtButton) { MouseButton button = mouseButtonFromEvent(evtButton.Button); - SDLButtonMask mask = SDL3.SDL_BUTTON(evtButton.Button); + SDLButtonMask mask = SDL_BUTTON(evtButton.Button); Debug.Assert(Enum.IsDefined(mask)); switch (evtButton.type) @@ -449,7 +449,7 @@ private void handleMouseButtonEvent(SDL_MouseButtonEvent evtButton) private void handleMouseMotionEvent(SDL_MouseMotionEvent evtMotion) { - if (SDL3.SDL_GetRelativeMouseMode() == SDL_bool.SDL_FALSE) + if (SDL_GetRelativeMouseMode() == SDL_bool.SDL_FALSE) MouseMove?.Invoke(new Vector2(evtMotion.x * Scale, evtMotion.y * Scale)); else MouseMoveRelative?.Invoke(new Vector2(evtMotion.xrel * Scale, evtMotion.yrel * Scale)); @@ -524,7 +524,7 @@ private MouseButton mouseButtonFromEvent(SDLButton button) /// Update the host window manager's cursor position based on a location relative to window coordinates. /// /// A position inside the window. - public unsafe void UpdateMousePosition(Vector2 mousePosition) => ScheduleCommand(() => SDL3.SDL_WarpMouseInWindow(SDLWindowHandle, mousePosition.X / Scale, mousePosition.Y / Scale)); + public unsafe void UpdateMousePosition(Vector2 mousePosition) => ScheduleCommand(() => SDL_WarpMouseInWindow(SDLWindowHandle, mousePosition.X / Scale, mousePosition.Y / Scale)); private void updateConfineMode() { diff --git a/osu.Framework/Platform/SDL3Window_Windowing.cs b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs similarity index 90% rename from osu.Framework/Platform/SDL3Window_Windowing.cs rename to osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs index a00954271f..fc66065215 100644 --- a/osu.Framework/Platform/SDL3Window_Windowing.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs @@ -11,11 +11,11 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Logging; -using osu.Framework.Platform.SDL; using osuTK; using SDL; +using static SDL.SDL3; -namespace osu.Framework.Platform +namespace osu.Framework.Platform.SDL3 { internal partial class SDL3Window { @@ -24,7 +24,7 @@ private unsafe void setupWindowing(FrameworkConfigManager config) config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss); minimiseOnFocusLoss.BindValueChanged(e => { - ScheduleCommand(() => SDL3.SDL_SetHint(SDL3.SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, e.NewValue ? "1"u8 : "0"u8)); + ScheduleCommand(() => SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, e.NewValue ? "1"u8 : "0"u8)); }, true); fetchDisplays(); @@ -69,7 +69,7 @@ private unsafe void setupWindowing(FrameworkConfigManager config) if (min.Width > sizeWindowed.MaxValue.Width || min.Height > sizeWindowed.MaxValue.Height) throw new InvalidOperationException($"Expected a size less than max window size ({sizeWindowed.MaxValue}), got {min}"); - ScheduleCommand(() => SDL3.SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height)); + ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height)); }; sizeWindowed.MaxValueChanged += max => @@ -80,7 +80,7 @@ private unsafe void setupWindowing(FrameworkConfigManager config) if (max.Width < sizeWindowed.MinValue.Width || max.Height < sizeWindowed.MinValue.Height) throw new InvalidOperationException($"Expected a size greater than min window size ({sizeWindowed.MinValue}), got {max}"); - ScheduleCommand(() => SDL3.SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height)); + ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height)); }; config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen); @@ -167,7 +167,7 @@ public unsafe Point Position set { position = value; - ScheduleCommand(() => SDL3.SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y)); + ScheduleCommand(() => SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y)); } } @@ -185,7 +185,7 @@ public unsafe bool Resizable return; resizable = value; - ScheduleCommand(() => SDL3.SDL_SetWindowResizable(SDLWindowHandle, value ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); + ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); } } @@ -247,9 +247,9 @@ public unsafe bool Visible ScheduleCommand(() => { if (value) - SDL3.SDL_ShowWindow(SDLWindowHandle); + SDL_ShowWindow(SDLWindowHandle); else - SDL3.SDL_HideWindow(SDLWindowHandle); + SDL_HideWindow(SDLWindowHandle); }); } } @@ -332,10 +332,10 @@ private void assertDisplaysMatchSDL() private static ImmutableArray getSDLDisplays() { - using var displays = SDL3.SDL_GetDisplays(); + using var displays = SDL_GetDisplays(); if (displays == null) - throw new InvalidOperationException($"Failed to get number of SDL displays. SDL Error: {SDL3.SDL_GetError()}"); + throw new InvalidOperationException($"Failed to get number of SDL displays. SDL Error: {SDL_GetError()}"); var builder = ImmutableArray.CreateBuilder(displays.Count); @@ -356,9 +356,9 @@ private static unsafe bool tryGetDisplayFromSDL(int displayIndex, SDL_DisplayID SDL_Rect rect; - if (SDL3.SDL_GetDisplayBounds(displayID, &rect) < 0) + if (SDL_GetDisplayBounds(displayID, &rect) < 0) { - Logger.Log($"Failed to get display bounds for display at index ({displayIndex}). SDL Error: {SDL3.SDL_GetError()}"); + Logger.Log($"Failed to get display bounds for display at index ({displayIndex}). SDL Error: {SDL_GetError()}"); display = null; return false; } @@ -367,11 +367,11 @@ private static unsafe bool tryGetDisplayFromSDL(int displayIndex, SDL_DisplayID if (RuntimeInfo.IsDesktop) { - using var modes = SDL3.SDL_GetFullscreenDisplayModes(displayID); + using var modes = SDL_GetFullscreenDisplayModes(displayID); if (modes == null) { - Logger.Log($"Failed to get display modes for display at index ({displayIndex}) ({rect.w}x{rect.h}). SDL Error: {SDL3.SDL_GetError()}"); + Logger.Log($"Failed to get display modes for display at index ({displayIndex}) ({rect.w}x{rect.h}). SDL Error: {SDL_GetError()}"); display = null; return false; } @@ -382,10 +382,10 @@ private static unsafe bool tryGetDisplayFromSDL(int displayIndex, SDL_DisplayID displayModes = new DisplayMode[modes.Count]; for (int i = 0; i < modes.Count; i++) - displayModes[i] = modes[i].ToDisplayMode(displayIndex); + displayModes[i] = SDL3Extensions.ToDisplayMode(modes[i], displayIndex); } - display = new Display(displayIndex, SDL3.SDL_GetDisplayName(displayID), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes); + display = new Display(displayIndex, SDL_GetDisplayName(displayID), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes); return true; } @@ -411,7 +411,7 @@ private unsafe Rectangle windowDisplayBounds get { SDL_Rect rect; - SDL3.SDL_GetDisplayBounds(displayID, &rect); + SDL_GetDisplayBounds(displayID, &rect); return new Rectangle(rect.x, rect.y, rect.w, rect.h); } } @@ -450,7 +450,7 @@ private unsafe Rectangle windowDisplayBounds private unsafe void fetchWindowSize() { int w, h; - SDL3.SDL_GetWindowSize(SDLWindowHandle, &w, &h); + SDL_GetWindowSize(SDLWindowHandle, &w, &h); int drawableW = graphicsSurface.GetDrawableSize().Width; @@ -476,7 +476,7 @@ private unsafe void handleWindowEvent(SDL_WindowEvent evtWindow) case SDL_EventType.SDL_EVENT_WINDOW_MOVED: // explicitly requery as there are occasions where what SDL has provided us with is not up-to-date. int x, y; - SDL3.SDL_GetWindowPosition(SDLWindowHandle, &x, &y); + SDL_GetWindowPosition(SDLWindowHandle, &x, &y); var newPosition = new Point(x, y); if (!newPosition.Equals(Position)) @@ -583,7 +583,7 @@ private unsafe void updateAndFetchWindowSpecifics() } else { - windowState = SDL3.SDL_GetWindowFlags(SDLWindowHandle).ToWindowState(); + windowState = SDL3Extensions.ToWindowState(SDL_GetWindowFlags(SDLWindowHandle)); } if (windowState != stateBefore) @@ -594,7 +594,7 @@ private unsafe void updateAndFetchWindowSpecifics() windowMaximised = maximized; } - var newDisplayID = SDL3.SDL_GetDisplayForWindow(SDLWindowHandle); + var newDisplayID = SDL_GetDisplayForWindow(SDLWindowHandle); if (displayID != newDisplayID) { @@ -611,10 +611,10 @@ private unsafe void updateAndFetchWindowSpecifics() private static bool tryGetDisplayIndex(SDL_DisplayID id, out int index) { - using var displays = SDL3.SDL_GetDisplays(); + using var displays = SDL_GetDisplays(); if (displays == null) - throw new InvalidOperationException($"Failed to get SDL displays. SDL error: {SDL3.SDL_GetError()}"); + throw new InvalidOperationException($"Failed to get SDL displays. SDL error: {SDL_GetError()}"); for (int i = 0; i < displays.Count; i++) { @@ -642,9 +642,9 @@ protected virtual unsafe void UpdateWindowStateAndSize(WindowState state, Displa case WindowState.Normal: Size = sizeWindowed.Value; - SDL3.SDL_RestoreWindow(SDLWindowHandle); - SDL3.SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height); - SDL3.SDL_SetWindowResizable(SDLWindowHandle, Resizable ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE); + SDL_RestoreWindow(SDLWindowHandle); + SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height); + SDL_SetWindowResizable(SDLWindowHandle, Resizable ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE); readWindowPositionFromConfig(state, display); break; @@ -656,8 +656,8 @@ protected virtual unsafe void UpdateWindowStateAndSize(WindowState state, Displa ensureWindowOnDisplay(display); - SDL3.SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode); - SDL3.SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_TRUE); + SDL_SetWindowFullscreenMode(SDLWindowHandle, &closestMode); + SDL_SetWindowFullscreen(SDLWindowHandle, SDL_bool.SDL_TRUE); break; case WindowState.FullscreenBorderless: @@ -665,16 +665,16 @@ protected virtual unsafe void UpdateWindowStateAndSize(WindowState state, Displa break; case WindowState.Maximised: - SDL3.SDL_RestoreWindow(SDLWindowHandle); + SDL_RestoreWindow(SDLWindowHandle); ensureWindowOnDisplay(display); - SDL3.SDL_MaximizeWindow(SDLWindowHandle); + SDL_MaximizeWindow(SDLWindowHandle); break; case WindowState.Minimised: ensureWindowOnDisplay(display); - SDL3.SDL_MinimizeWindow(SDLWindowHandle); + SDL_MinimizeWindow(SDLWindowHandle); break; } } @@ -687,7 +687,7 @@ private static unsafe bool tryFetchDisplayMode(SDL_Window* windowHandle, WindowS return false; } - var mode = windowState == WindowState.Fullscreen ? SDL3.SDL_GetWindowFullscreenMode(windowHandle) : SDL3.SDL_GetDesktopDisplayMode(displayID); + var mode = windowState == WindowState.Fullscreen ? SDL_GetWindowFullscreenMode(windowHandle) : SDL_GetDesktopDisplayMode(displayID); string type = windowState == WindowState.Fullscreen ? "fullscreen" : "desktop"; if (mode != null) @@ -698,7 +698,7 @@ private static unsafe bool tryFetchDisplayMode(SDL_Window* windowHandle, WindowS } else { - Logger.Log($"Failed to get {type} display mode. Display index: {display.Index}. SDL error: {SDL3.SDL_GetError()}"); + Logger.Log($"Failed to get {type} display mode. Display index: {display.Index}. SDL error: {SDL_GetError()}"); displayMode = default; return false; } @@ -734,7 +734,7 @@ private unsafe void ensureWindowOnDisplay(Display display) { if (tryGetDisplayAtIndex(display.Index, out var requestedID)) { - if (requestedID == SDL3.SDL_GetDisplayForWindow(SDLWindowHandle)) + if (requestedID == SDL_GetDisplayForWindow(SDLWindowHandle)) return; } @@ -846,10 +846,10 @@ private static bool tryGetDisplayAtIndex(int index, out SDL_DisplayID displayID) { ArgumentOutOfRangeException.ThrowIfNegative(index); - using var displays = SDL3.SDL_GetDisplays(); + using var displays = SDL_GetDisplays(); if (displays == null) - throw new InvalidOperationException($"Unable to get displays. SDL error: {SDL3.SDL_GetError()}"); + throw new InvalidOperationException($"Unable to get displays. SDL error: {SDL_GetError()}"); if (index >= displays.Count) { @@ -863,7 +863,7 @@ private static bool tryGetDisplayAtIndex(int index, out SDL_DisplayID displayID) private static unsafe SDL_DisplayMode getClosestDisplayMode(SDL_Window* windowHandle, Size size, Display display, DisplayMode requestedMode) { - SDL3.SDL_ClearError(); // clear any stale error. + SDL_ClearError(); // clear any stale error. if (!tryGetDisplayAtIndex(display.Index, out var displayID)) throw new ArgumentException($"Requested display index ({display}) is invalid.", nameof(display)); @@ -872,28 +872,28 @@ private static unsafe SDL_DisplayMode getClosestDisplayMode(SDL_Window* windowHa if (size.Width == 9999 && size.Height == 9999) size = display.Bounds.Size; - var mode = SDL3.SDL_GetClosestFullscreenDisplayMode(displayID, size.Width, size.Height, requestedMode.RefreshRate, SDL_bool.SDL_TRUE); + var mode = SDL_GetClosestFullscreenDisplayMode(displayID, size.Width, size.Height, requestedMode.RefreshRate, SDL_bool.SDL_TRUE); if (mode != null) return *mode; else Logger.Log($"Unable to get preferred display mode (try #1/2). Target display: {display.Index}, mode: {size.Width}x{size.Height}@{requestedMode.RefreshRate}. SDL error: {SDL3Extensions.GetAndClearError()}"); // fallback to current display's native bounds - mode = SDL3.SDL_GetClosestFullscreenDisplayMode(displayID, display.Bounds.Width, display.Bounds.Height, 0f, SDL_bool.SDL_TRUE); + mode = SDL_GetClosestFullscreenDisplayMode(displayID, display.Bounds.Width, display.Bounds.Height, 0f, SDL_bool.SDL_TRUE); if (mode != null) return *mode; else Logger.Log($"Unable to get preferred display mode (try #2/2). Target display: {display.Index}, mode: {display.Bounds.Width}x{display.Bounds.Height}@default. SDL error: {SDL3Extensions.GetAndClearError()}"); // try the display's native display mode. - mode = SDL3.SDL_GetDesktopDisplayMode(displayID); + mode = SDL_GetDesktopDisplayMode(displayID); if (mode != null) return *mode; else Logger.Log($"Failed to get desktop display mode (try #1/1). Target display: {display.Index}. SDL error: {SDL3Extensions.GetAndClearError()}", level: LogLevel.Error); // finally return the current mode if everything else fails. - mode = SDL3.SDL_GetWindowFullscreenMode(windowHandle); + mode = SDL_GetWindowFullscreenMode(windowHandle); if (mode != null) return *mode; else diff --git a/osu.Framework/Platform/SDL3GameHost.cs b/osu.Framework/Platform/SDL3GameHost.cs index 22267963d6..57ef866851 100644 --- a/osu.Framework/Platform/SDL3GameHost.cs +++ b/osu.Framework/Platform/SDL3GameHost.cs @@ -10,7 +10,7 @@ using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Touch; -using osu.Framework.Platform.SDL; +using osu.Framework.Platform.SDL3; using SixLabors.ImageSharp.Formats.Png; namespace osu.Framework.Platform diff --git a/osu.Framework/Platform/Windows/WindowsReadableKeyCombinationProvider.cs b/osu.Framework/Platform/Windows/WindowsReadableKeyCombinationProvider.cs index fffe665478..b8e58dca9a 100644 --- a/osu.Framework/Platform/Windows/WindowsReadableKeyCombinationProvider.cs +++ b/osu.Framework/Platform/Windows/WindowsReadableKeyCombinationProvider.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Input.Bindings; -using osu.Framework.Platform.SDL; +using osu.Framework.Platform.SDL3; using SDL; namespace osu.Framework.Platform.Windows diff --git a/osu.Framework/Platform/Windows/WindowsWindow.cs b/osu.Framework/Platform/Windows/WindowsWindow.cs index a1e4ffb8fd..f0b3171474 100644 --- a/osu.Framework/Platform/Windows/WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/WindowsWindow.cs @@ -9,12 +9,13 @@ using System.Runtime.Versioning; using osu.Framework.Allocation; using osu.Framework.Input.Handlers.Mouse; -using osu.Framework.Platform.SDL; +using osu.Framework.Platform.SDL3; using osu.Framework.Platform.Windows.Native; using osuTK; using osuTK.Input; using SDL; using Icon = osu.Framework.Platform.Windows.Native.Icon; +using static SDL.SDL3; namespace osu.Framework.Platform.Windows { @@ -63,7 +64,7 @@ public override void Create() public override unsafe void Run() { - SDL3.SDL_SetWindowsMessageHook(&messageHook, ObjectHandle.Handle); + SDL_SetWindowsMessageHook(&messageHook, ObjectHandle.Handle); base.Run(); } @@ -123,7 +124,7 @@ private void warpCursorFromFocusLoss() && RelativeMouseMode) { var pt = PointToScreen(new Point((int)LastMousePosition.Value.X, (int)LastMousePosition.Value.Y)); - SDL3.SDL_WarpMouseGlobal(pt.X, pt.Y); // this directly calls the SetCursorPos win32 API + SDL_WarpMouseGlobal(pt.X, pt.Y); // this directly calls the SetCursorPos win32 API } } @@ -261,7 +262,7 @@ protected set protected override unsafe Size SetBorderless(Display display) { - SDL3.SDL_SetWindowBordered(SDLWindowHandle, SDL_bool.SDL_FALSE); + SDL_SetWindowBordered(SDLWindowHandle, SDL_bool.SDL_FALSE); var newSize = display.Bounds.Size; @@ -270,7 +271,7 @@ protected override unsafe Size SetBorderless(Display display) // we also trick the game into thinking the window has normal size: see Size setter override newSize += new Size(windows_borderless_width_hack, 0); - SDL3.SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height); + SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height); Position = display.Bounds.Location; return newSize; From dd66f3e6b45600088bd0475ded7a9fce42621b56 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 16:52:53 +0900 Subject: [PATCH 040/140] Add SDL2 classes --- osu.Framework/Input/SDL2WindowTextInput.cs | 70 + osu.Framework/Platform/SDL2/SDL2Clipboard.cs | 28 + .../Platform/SDL2/SDL2ControllerBindings.cs | 79 ++ .../Platform/SDL2/SDL2DesktopWindow.cs | 27 + osu.Framework/Platform/SDL2/SDL2Extensions.cs | 1161 +++++++++++++++++ .../Platform/SDL2/SDL2GraphicsSurface.cs | 216 +++ .../SDL2ReadableKeyCombinationProvider.cs | 230 ++++ osu.Framework/Platform/SDL2/SDL2Structs.cs | 50 + osu.Framework/Platform/SDL2/SDL2Window.cs | 680 ++++++++++ .../Platform/SDL2/SDL2Window_Input.cs | 665 ++++++++++ .../Platform/SDL2/SDL2Window_Windowing.cs | 889 +++++++++++++ osu.Framework/Platform/SDL2GameHost.cs | 48 + 12 files changed, 4143 insertions(+) create mode 100644 osu.Framework/Input/SDL2WindowTextInput.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2Clipboard.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2ControllerBindings.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2Extensions.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2GraphicsSurface.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2Structs.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2Window.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2Window_Input.cs create mode 100644 osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs create mode 100644 osu.Framework/Platform/SDL2GameHost.cs diff --git a/osu.Framework/Input/SDL2WindowTextInput.cs b/osu.Framework/Input/SDL2WindowTextInput.cs new file mode 100644 index 0000000000..77e7accfee --- /dev/null +++ b/osu.Framework/Input/SDL2WindowTextInput.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Primitives; +using osu.Framework.Platform.SDL2; + +namespace osu.Framework.Input +{ + internal class SDL2WindowTextInput : TextInputSource + { + private readonly SDL2Window window; + + public SDL2WindowTextInput(SDL2Window window) + { + this.window = window; + } + + private void handleTextInput(string text) + { + // SDL sends IME results as `SDL_TextInputEvent` which we can't differentiate from regular text input + // so we have to manually keep track and invoke the correct event. + + if (ImeActive) + { + TriggerImeResult(text); + } + else + { + TriggerTextInput(text); + } + } + + private void handleTextEditing(string? text, int selectionStart, int selectionLength) + { + if (text == null) return; + + TriggerImeComposition(text, selectionStart, selectionLength); + } + + protected override void ActivateTextInput(bool allowIme) + { + window.TextInput += handleTextInput; + window.TextEditing += handleTextEditing; + window.StartTextInput(allowIme); + } + + protected override void EnsureTextInputActivated(bool allowIme) + { + window.StartTextInput(allowIme); + } + + protected override void DeactivateTextInput() + { + window.TextInput -= handleTextInput; + window.TextEditing -= handleTextEditing; + window.StopTextInput(); + } + + public override void SetImeRectangle(RectangleF rectangle) + { + window.SetTextInputRect(rectangle); + } + + public override void ResetIme() + { + base.ResetIme(); + window.ResetIme(); + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2Clipboard.cs b/osu.Framework/Platform/SDL2/SDL2Clipboard.cs new file mode 100644 index 0000000000..f75a4091b5 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2Clipboard.cs @@ -0,0 +1,28 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using SixLabors.ImageSharp; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + public class SDL2Clipboard : Clipboard + { + // SDL cannot differentiate between string.Empty and no text (eg. empty clipboard or an image) + // doesn't matter as text editors don't really allow copying empty strings. + // assume that empty text means no text. + public override string? GetText() => SDL_HasClipboardText() == SDL_bool.SDL_TRUE ? SDL_GetClipboardText() : null; + + public override void SetText(string text) => SDL_SetClipboardText(text); + + public override Image? GetImage() + { + return null; + } + + public override bool SetImage(Image image) + { + return false; + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2ControllerBindings.cs b/osu.Framework/Platform/SDL2/SDL2ControllerBindings.cs new file mode 100644 index 0000000000..fbf06b7cb3 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2ControllerBindings.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Linq; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + /// + /// Maintain a copy of the SDL-provided bindings for the given controller. + /// Used to determine whether a given event's joystick button or axis is unmapped. + /// + internal class SDL2ControllerBindings + { + public readonly IntPtr JoystickHandle; + public readonly IntPtr ControllerHandle; + + /// + /// Bindings returned from , indexed by . + /// Empty if the joystick does not have a corresponding ControllerHandle. + /// + public SDL_GameControllerButtonBind[] ButtonBindings; + + /// + /// Bindings returned from , indexed by . + /// Empty if the joystick does not have a corresponding ControllerHandle. + /// + public SDL_GameControllerButtonBind[] AxisBindings; + + public SDL2ControllerBindings(IntPtr joystickHandle, IntPtr controllerHandle) + { + JoystickHandle = joystickHandle; + ControllerHandle = controllerHandle; + + PopulateBindings(); + } + + public void PopulateBindings() + { + if (ControllerHandle == IntPtr.Zero) + { + ButtonBindings = Array.Empty(); + AxisBindings = Array.Empty(); + return; + } + + ButtonBindings = Enumerable.Range(0, (int)SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_MAX) + .Select(i => SDL_GameControllerGetBindForButton(ControllerHandle, (SDL_GameControllerButton)i)).ToArray(); + + AxisBindings = Enumerable.Range(0, (int)SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_MAX) + .Select(i => SDL_GameControllerGetBindForAxis(ControllerHandle, (SDL_GameControllerAxis)i)).ToArray(); + } + + public bool IsJoystickButtonBound(byte buttonIndex) + { + for (int i = 0; i < ButtonBindings.Length; i++) + { + if (ButtonBindings[i].bindType != SDL_GameControllerBindType.SDL_CONTROLLER_BINDTYPE_NONE && ButtonBindings[i].value.button == buttonIndex) + return true; + } + + return false; + } + + public bool IsJoystickAxisBound(byte axisIndex) + { + for (int i = 0; i < AxisBindings.Length; i++) + { + if (AxisBindings[i].bindType != SDL_GameControllerBindType.SDL_CONTROLLER_BINDTYPE_NONE && AxisBindings[i].value.axis == axisIndex) + return true; + } + + return false; + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs b/osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs new file mode 100644 index 0000000000..94c5795417 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + internal class SDL2DesktopWindow : SDL2Window + { + public SDL2DesktopWindow(GraphicsSurfaceType surfaceType) + : base(surfaceType) + { + } + + protected override void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode) + { + // this reset is required even on changing from one fullscreen resolution to another. + // if it is not included, the GL context will not get the correct size. + // this is mentioned by multiple sources as an SDL issue, which seems to resolve by similar means (see https://discourse.libsdl.org/t/sdl-setwindowsize-does-not-work-in-fullscreen/20711/4). + SDL_SetWindowBordered(SDLWindowHandle, SDL_bool.SDL_TRUE); + SDL_SetWindowFullscreen(SDLWindowHandle, (uint)SDL_bool.SDL_FALSE); + SDL_RestoreWindow(SDLWindowHandle); + + base.UpdateWindowStateAndSize(state, display, displayMode); + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2Extensions.cs b/osu.Framework/Platform/SDL2/SDL2Extensions.cs new file mode 100644 index 0000000000..edfd63b6f8 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2Extensions.cs @@ -0,0 +1,1161 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osuTK.Input; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + public static class SDL2Extensions + { + public static Key ToKey(this SDL_Keysym sdlKeysym) + { + // Apple devices don't have the notion of NumLock (they have a Clear key instead). + // treat them as if they always have NumLock on (the numpad always performs its primary actions). + bool numLockOn = sdlKeysym.mod.HasFlagFast(SDL_Keymod.KMOD_NUM) || RuntimeInfo.IsApple; + + switch (sdlKeysym.scancode) + { + default: + case SDL_Scancode.SDL_SCANCODE_UNKNOWN: + return Key.Unknown; + + case SDL_Scancode.SDL_SCANCODE_KP_COMMA: + return Key.Comma; + + case SDL_Scancode.SDL_SCANCODE_KP_TAB: + return Key.Tab; + + case SDL_Scancode.SDL_SCANCODE_KP_BACKSPACE: + return Key.BackSpace; + + case SDL_Scancode.SDL_SCANCODE_KP_A: + return Key.A; + + case SDL_Scancode.SDL_SCANCODE_KP_B: + return Key.B; + + case SDL_Scancode.SDL_SCANCODE_KP_C: + return Key.C; + + case SDL_Scancode.SDL_SCANCODE_KP_D: + return Key.D; + + case SDL_Scancode.SDL_SCANCODE_KP_E: + return Key.E; + + case SDL_Scancode.SDL_SCANCODE_KP_F: + return Key.F; + + case SDL_Scancode.SDL_SCANCODE_KP_SPACE: + return Key.Space; + + case SDL_Scancode.SDL_SCANCODE_KP_CLEAR: + return Key.Clear; + + case SDL_Scancode.SDL_SCANCODE_RETURN: + return Key.Enter; + + case SDL_Scancode.SDL_SCANCODE_ESCAPE: + return Key.Escape; + + case SDL_Scancode.SDL_SCANCODE_BACKSPACE: + return Key.BackSpace; + + case SDL_Scancode.SDL_SCANCODE_TAB: + return Key.Tab; + + case SDL_Scancode.SDL_SCANCODE_SPACE: + return Key.Space; + + case SDL_Scancode.SDL_SCANCODE_APOSTROPHE: + return Key.Quote; + + case SDL_Scancode.SDL_SCANCODE_COMMA: + return Key.Comma; + + case SDL_Scancode.SDL_SCANCODE_MINUS: + return Key.Minus; + + case SDL_Scancode.SDL_SCANCODE_PERIOD: + return Key.Period; + + case SDL_Scancode.SDL_SCANCODE_SLASH: + return Key.Slash; + + case SDL_Scancode.SDL_SCANCODE_0: + return Key.Number0; + + case SDL_Scancode.SDL_SCANCODE_1: + return Key.Number1; + + case SDL_Scancode.SDL_SCANCODE_2: + return Key.Number2; + + case SDL_Scancode.SDL_SCANCODE_3: + return Key.Number3; + + case SDL_Scancode.SDL_SCANCODE_4: + return Key.Number4; + + case SDL_Scancode.SDL_SCANCODE_5: + return Key.Number5; + + case SDL_Scancode.SDL_SCANCODE_6: + return Key.Number6; + + case SDL_Scancode.SDL_SCANCODE_7: + return Key.Number7; + + case SDL_Scancode.SDL_SCANCODE_8: + return Key.Number8; + + case SDL_Scancode.SDL_SCANCODE_9: + return Key.Number9; + + case SDL_Scancode.SDL_SCANCODE_SEMICOLON: + return Key.Semicolon; + + case SDL_Scancode.SDL_SCANCODE_EQUALS: + return Key.Plus; + + case SDL_Scancode.SDL_SCANCODE_LEFTBRACKET: + return Key.BracketLeft; + + case SDL_Scancode.SDL_SCANCODE_BACKSLASH: + return Key.BackSlash; + + case SDL_Scancode.SDL_SCANCODE_RIGHTBRACKET: + return Key.BracketRight; + + case SDL_Scancode.SDL_SCANCODE_GRAVE: + return Key.Tilde; + + case SDL_Scancode.SDL_SCANCODE_A: + return Key.A; + + case SDL_Scancode.SDL_SCANCODE_B: + return Key.B; + + case SDL_Scancode.SDL_SCANCODE_C: + return Key.C; + + case SDL_Scancode.SDL_SCANCODE_D: + return Key.D; + + case SDL_Scancode.SDL_SCANCODE_E: + return Key.E; + + case SDL_Scancode.SDL_SCANCODE_F: + return Key.F; + + case SDL_Scancode.SDL_SCANCODE_G: + return Key.G; + + case SDL_Scancode.SDL_SCANCODE_H: + return Key.H; + + case SDL_Scancode.SDL_SCANCODE_I: + return Key.I; + + case SDL_Scancode.SDL_SCANCODE_J: + return Key.J; + + case SDL_Scancode.SDL_SCANCODE_K: + return Key.K; + + case SDL_Scancode.SDL_SCANCODE_L: + return Key.L; + + case SDL_Scancode.SDL_SCANCODE_M: + return Key.M; + + case SDL_Scancode.SDL_SCANCODE_N: + return Key.N; + + case SDL_Scancode.SDL_SCANCODE_O: + return Key.O; + + case SDL_Scancode.SDL_SCANCODE_P: + return Key.P; + + case SDL_Scancode.SDL_SCANCODE_Q: + return Key.Q; + + case SDL_Scancode.SDL_SCANCODE_R: + return Key.R; + + case SDL_Scancode.SDL_SCANCODE_S: + return Key.S; + + case SDL_Scancode.SDL_SCANCODE_T: + return Key.T; + + case SDL_Scancode.SDL_SCANCODE_U: + return Key.U; + + case SDL_Scancode.SDL_SCANCODE_V: + return Key.V; + + case SDL_Scancode.SDL_SCANCODE_W: + return Key.W; + + case SDL_Scancode.SDL_SCANCODE_X: + return Key.X; + + case SDL_Scancode.SDL_SCANCODE_Y: + return Key.Y; + + case SDL_Scancode.SDL_SCANCODE_Z: + return Key.Z; + + case SDL_Scancode.SDL_SCANCODE_CAPSLOCK: + return Key.CapsLock; + + case SDL_Scancode.SDL_SCANCODE_F1: + return Key.F1; + + case SDL_Scancode.SDL_SCANCODE_F2: + return Key.F2; + + case SDL_Scancode.SDL_SCANCODE_F3: + return Key.F3; + + case SDL_Scancode.SDL_SCANCODE_F4: + return Key.F4; + + case SDL_Scancode.SDL_SCANCODE_F5: + return Key.F5; + + case SDL_Scancode.SDL_SCANCODE_F6: + return Key.F6; + + case SDL_Scancode.SDL_SCANCODE_F7: + return Key.F7; + + case SDL_Scancode.SDL_SCANCODE_F8: + return Key.F8; + + case SDL_Scancode.SDL_SCANCODE_F9: + return Key.F9; + + case SDL_Scancode.SDL_SCANCODE_F10: + return Key.F10; + + case SDL_Scancode.SDL_SCANCODE_F11: + return Key.F11; + + case SDL_Scancode.SDL_SCANCODE_F12: + return Key.F12; + + case SDL_Scancode.SDL_SCANCODE_PRINTSCREEN: + return Key.PrintScreen; + + case SDL_Scancode.SDL_SCANCODE_SCROLLLOCK: + return Key.ScrollLock; + + case SDL_Scancode.SDL_SCANCODE_PAUSE: + return Key.Pause; + + case SDL_Scancode.SDL_SCANCODE_INSERT: + return Key.Insert; + + case SDL_Scancode.SDL_SCANCODE_HOME: + return Key.Home; + + case SDL_Scancode.SDL_SCANCODE_PAGEUP: + return Key.PageUp; + + case SDL_Scancode.SDL_SCANCODE_DELETE: + return Key.Delete; + + case SDL_Scancode.SDL_SCANCODE_END: + return Key.End; + + case SDL_Scancode.SDL_SCANCODE_PAGEDOWN: + return Key.PageDown; + + case SDL_Scancode.SDL_SCANCODE_RIGHT: + return Key.Right; + + case SDL_Scancode.SDL_SCANCODE_LEFT: + return Key.Left; + + case SDL_Scancode.SDL_SCANCODE_DOWN: + return Key.Down; + + case SDL_Scancode.SDL_SCANCODE_UP: + return Key.Up; + + case SDL_Scancode.SDL_SCANCODE_NUMLOCKCLEAR: + return Key.NumLock; + + case SDL_Scancode.SDL_SCANCODE_KP_DIVIDE: + return Key.KeypadDivide; + + case SDL_Scancode.SDL_SCANCODE_KP_MULTIPLY: + return Key.KeypadMultiply; + + case SDL_Scancode.SDL_SCANCODE_KP_MINUS: + return Key.KeypadMinus; + + case SDL_Scancode.SDL_SCANCODE_KP_PLUS: + return Key.KeypadPlus; + + case SDL_Scancode.SDL_SCANCODE_KP_ENTER: + return Key.KeypadEnter; + + case SDL_Scancode.SDL_SCANCODE_KP_1: + return numLockOn ? Key.Keypad1 : Key.End; + + case SDL_Scancode.SDL_SCANCODE_KP_2: + return numLockOn ? Key.Keypad2 : Key.Down; + + case SDL_Scancode.SDL_SCANCODE_KP_3: + return numLockOn ? Key.Keypad3 : Key.PageDown; + + case SDL_Scancode.SDL_SCANCODE_KP_4: + return numLockOn ? Key.Keypad4 : Key.Left; + + case SDL_Scancode.SDL_SCANCODE_KP_5: + return numLockOn ? Key.Keypad5 : Key.Clear; + + case SDL_Scancode.SDL_SCANCODE_KP_6: + return numLockOn ? Key.Keypad6 : Key.Right; + + case SDL_Scancode.SDL_SCANCODE_KP_7: + return numLockOn ? Key.Keypad7 : Key.Home; + + case SDL_Scancode.SDL_SCANCODE_KP_8: + return numLockOn ? Key.Keypad8 : Key.Up; + + case SDL_Scancode.SDL_SCANCODE_KP_9: + return numLockOn ? Key.Keypad9 : Key.PageUp; + + case SDL_Scancode.SDL_SCANCODE_KP_0: + return numLockOn ? Key.Keypad0 : Key.Insert; + + case SDL_Scancode.SDL_SCANCODE_KP_PERIOD: + return numLockOn ? Key.KeypadPeriod : Key.Delete; + + case SDL_Scancode.SDL_SCANCODE_NONUSBACKSLASH: + return Key.NonUSBackSlash; + + case SDL_Scancode.SDL_SCANCODE_F13: + return Key.F13; + + case SDL_Scancode.SDL_SCANCODE_F14: + return Key.F14; + + case SDL_Scancode.SDL_SCANCODE_F15: + return Key.F15; + + case SDL_Scancode.SDL_SCANCODE_F16: + return Key.F16; + + case SDL_Scancode.SDL_SCANCODE_F17: + return Key.F17; + + case SDL_Scancode.SDL_SCANCODE_F18: + return Key.F18; + + case SDL_Scancode.SDL_SCANCODE_F19: + return Key.F19; + + case SDL_Scancode.SDL_SCANCODE_F20: + return Key.F20; + + case SDL_Scancode.SDL_SCANCODE_F21: + return Key.F21; + + case SDL_Scancode.SDL_SCANCODE_F22: + return Key.F22; + + case SDL_Scancode.SDL_SCANCODE_F23: + return Key.F23; + + case SDL_Scancode.SDL_SCANCODE_F24: + return Key.F24; + + case SDL_Scancode.SDL_SCANCODE_MENU: + case SDL_Scancode.SDL_SCANCODE_APPLICATION: + return Key.Menu; + + case SDL_Scancode.SDL_SCANCODE_STOP: + return Key.Stop; + + case SDL_Scancode.SDL_SCANCODE_MUTE: + return Key.Mute; + + case SDL_Scancode.SDL_SCANCODE_VOLUMEUP: + return Key.VolumeUp; + + case SDL_Scancode.SDL_SCANCODE_VOLUMEDOWN: + return Key.VolumeDown; + + case SDL_Scancode.SDL_SCANCODE_CLEAR: + return Key.Clear; + + case SDL_Scancode.SDL_SCANCODE_DECIMALSEPARATOR: + return Key.KeypadDecimal; + + case SDL_Scancode.SDL_SCANCODE_LCTRL: + return Key.ControlLeft; + + case SDL_Scancode.SDL_SCANCODE_LSHIFT: + return Key.ShiftLeft; + + case SDL_Scancode.SDL_SCANCODE_LALT: + return Key.AltLeft; + + case SDL_Scancode.SDL_SCANCODE_LGUI: + return Key.WinLeft; + + case SDL_Scancode.SDL_SCANCODE_RCTRL: + return Key.ControlRight; + + case SDL_Scancode.SDL_SCANCODE_RSHIFT: + return Key.ShiftRight; + + case SDL_Scancode.SDL_SCANCODE_RALT: + return Key.AltRight; + + case SDL_Scancode.SDL_SCANCODE_RGUI: + return Key.WinRight; + + case SDL_Scancode.SDL_SCANCODE_AUDIONEXT: + return Key.TrackNext; + + case SDL_Scancode.SDL_SCANCODE_AUDIOPREV: + return Key.TrackPrevious; + + case SDL_Scancode.SDL_SCANCODE_AUDIOSTOP: + return Key.Stop; + + case SDL_Scancode.SDL_SCANCODE_AUDIOPLAY: + return Key.PlayPause; + + case SDL_Scancode.SDL_SCANCODE_AUDIOMUTE: + return Key.Mute; + + case SDL_Scancode.SDL_SCANCODE_SLEEP: + return Key.Sleep; + } + } + + /// + /// Returns the corresponding for a given . + /// + /// + /// Should be a keyboard key. + /// + /// + /// The corresponding if the is valid. + /// otherwise. + /// + public static SDL_Scancode ToScancode(this InputKey inputKey) + { + switch (inputKey) + { + default: + case InputKey.Shift: + case InputKey.Control: + case InputKey.Alt: + case InputKey.Super: + case InputKey.F25: + case InputKey.F26: + case InputKey.F27: + case InputKey.F28: + case InputKey.F29: + case InputKey.F30: + case InputKey.F31: + case InputKey.F32: + case InputKey.F33: + case InputKey.F34: + case InputKey.F35: + case InputKey.Clear: + return SDL_Scancode.SDL_SCANCODE_UNKNOWN; + + case InputKey.Menu: + return SDL_Scancode.SDL_SCANCODE_MENU; + + case InputKey.F1: + return SDL_Scancode.SDL_SCANCODE_F1; + + case InputKey.F2: + return SDL_Scancode.SDL_SCANCODE_F2; + + case InputKey.F3: + return SDL_Scancode.SDL_SCANCODE_F3; + + case InputKey.F4: + return SDL_Scancode.SDL_SCANCODE_F4; + + case InputKey.F5: + return SDL_Scancode.SDL_SCANCODE_F5; + + case InputKey.F6: + return SDL_Scancode.SDL_SCANCODE_F6; + + case InputKey.F7: + return SDL_Scancode.SDL_SCANCODE_F7; + + case InputKey.F8: + return SDL_Scancode.SDL_SCANCODE_F8; + + case InputKey.F9: + return SDL_Scancode.SDL_SCANCODE_F9; + + case InputKey.F10: + return SDL_Scancode.SDL_SCANCODE_F10; + + case InputKey.F11: + return SDL_Scancode.SDL_SCANCODE_F11; + + case InputKey.F12: + return SDL_Scancode.SDL_SCANCODE_F12; + + case InputKey.F13: + return SDL_Scancode.SDL_SCANCODE_F13; + + case InputKey.F14: + return SDL_Scancode.SDL_SCANCODE_F14; + + case InputKey.F15: + return SDL_Scancode.SDL_SCANCODE_F15; + + case InputKey.F16: + return SDL_Scancode.SDL_SCANCODE_F16; + + case InputKey.F17: + return SDL_Scancode.SDL_SCANCODE_F17; + + case InputKey.F18: + return SDL_Scancode.SDL_SCANCODE_F18; + + case InputKey.F19: + return SDL_Scancode.SDL_SCANCODE_F19; + + case InputKey.F20: + return SDL_Scancode.SDL_SCANCODE_F20; + + case InputKey.F21: + return SDL_Scancode.SDL_SCANCODE_F21; + + case InputKey.F22: + return SDL_Scancode.SDL_SCANCODE_F22; + + case InputKey.F23: + return SDL_Scancode.SDL_SCANCODE_F23; + + case InputKey.F24: + return SDL_Scancode.SDL_SCANCODE_F24; + + case InputKey.Up: + return SDL_Scancode.SDL_SCANCODE_UP; + + case InputKey.Down: + return SDL_Scancode.SDL_SCANCODE_DOWN; + + case InputKey.Left: + return SDL_Scancode.SDL_SCANCODE_LEFT; + + case InputKey.Right: + return SDL_Scancode.SDL_SCANCODE_RIGHT; + + case InputKey.Enter: + return SDL_Scancode.SDL_SCANCODE_RETURN; + + case InputKey.Escape: + return SDL_Scancode.SDL_SCANCODE_ESCAPE; + + case InputKey.Space: + return SDL_Scancode.SDL_SCANCODE_SPACE; + + case InputKey.Tab: + return SDL_Scancode.SDL_SCANCODE_TAB; + + case InputKey.BackSpace: + return SDL_Scancode.SDL_SCANCODE_BACKSPACE; + + case InputKey.Insert: + return SDL_Scancode.SDL_SCANCODE_INSERT; + + case InputKey.Delete: + return SDL_Scancode.SDL_SCANCODE_DELETE; + + case InputKey.PageUp: + return SDL_Scancode.SDL_SCANCODE_PAGEUP; + + case InputKey.PageDown: + return SDL_Scancode.SDL_SCANCODE_PAGEDOWN; + + case InputKey.Home: + return SDL_Scancode.SDL_SCANCODE_HOME; + + case InputKey.End: + return SDL_Scancode.SDL_SCANCODE_END; + + case InputKey.CapsLock: + return SDL_Scancode.SDL_SCANCODE_CAPSLOCK; + + case InputKey.ScrollLock: + return SDL_Scancode.SDL_SCANCODE_SCROLLLOCK; + + case InputKey.PrintScreen: + return SDL_Scancode.SDL_SCANCODE_PRINTSCREEN; + + case InputKey.Pause: + return SDL_Scancode.SDL_SCANCODE_PAUSE; + + case InputKey.NumLock: + return SDL_Scancode.SDL_SCANCODE_NUMLOCKCLEAR; + + case InputKey.Sleep: + return SDL_Scancode.SDL_SCANCODE_SLEEP; + + case InputKey.Keypad0: + return SDL_Scancode.SDL_SCANCODE_KP_0; + + case InputKey.Keypad1: + return SDL_Scancode.SDL_SCANCODE_KP_1; + + case InputKey.Keypad2: + return SDL_Scancode.SDL_SCANCODE_KP_2; + + case InputKey.Keypad3: + return SDL_Scancode.SDL_SCANCODE_KP_3; + + case InputKey.Keypad4: + return SDL_Scancode.SDL_SCANCODE_KP_4; + + case InputKey.Keypad5: + return SDL_Scancode.SDL_SCANCODE_KP_5; + + case InputKey.Keypad6: + return SDL_Scancode.SDL_SCANCODE_KP_6; + + case InputKey.Keypad7: + return SDL_Scancode.SDL_SCANCODE_KP_7; + + case InputKey.Keypad8: + return SDL_Scancode.SDL_SCANCODE_KP_8; + + case InputKey.Keypad9: + return SDL_Scancode.SDL_SCANCODE_KP_9; + + case InputKey.KeypadDivide: + return SDL_Scancode.SDL_SCANCODE_KP_DIVIDE; + + case InputKey.KeypadMultiply: + return SDL_Scancode.SDL_SCANCODE_KP_MULTIPLY; + + case InputKey.KeypadMinus: + return SDL_Scancode.SDL_SCANCODE_KP_MINUS; + + case InputKey.KeypadPlus: + return SDL_Scancode.SDL_SCANCODE_KP_PLUS; + + case InputKey.KeypadPeriod: + return SDL_Scancode.SDL_SCANCODE_KP_PERIOD; + + case InputKey.KeypadEnter: + return SDL_Scancode.SDL_SCANCODE_KP_ENTER; + + case InputKey.A: + return SDL_Scancode.SDL_SCANCODE_A; + + case InputKey.B: + return SDL_Scancode.SDL_SCANCODE_B; + + case InputKey.C: + return SDL_Scancode.SDL_SCANCODE_C; + + case InputKey.D: + return SDL_Scancode.SDL_SCANCODE_D; + + case InputKey.E: + return SDL_Scancode.SDL_SCANCODE_E; + + case InputKey.F: + return SDL_Scancode.SDL_SCANCODE_F; + + case InputKey.G: + return SDL_Scancode.SDL_SCANCODE_G; + + case InputKey.H: + return SDL_Scancode.SDL_SCANCODE_H; + + case InputKey.I: + return SDL_Scancode.SDL_SCANCODE_I; + + case InputKey.J: + return SDL_Scancode.SDL_SCANCODE_J; + + case InputKey.K: + return SDL_Scancode.SDL_SCANCODE_K; + + case InputKey.L: + return SDL_Scancode.SDL_SCANCODE_L; + + case InputKey.M: + return SDL_Scancode.SDL_SCANCODE_M; + + case InputKey.N: + return SDL_Scancode.SDL_SCANCODE_N; + + case InputKey.O: + return SDL_Scancode.SDL_SCANCODE_O; + + case InputKey.P: + return SDL_Scancode.SDL_SCANCODE_P; + + case InputKey.Q: + return SDL_Scancode.SDL_SCANCODE_Q; + + case InputKey.R: + return SDL_Scancode.SDL_SCANCODE_R; + + case InputKey.S: + return SDL_Scancode.SDL_SCANCODE_S; + + case InputKey.T: + return SDL_Scancode.SDL_SCANCODE_T; + + case InputKey.U: + return SDL_Scancode.SDL_SCANCODE_U; + + case InputKey.V: + return SDL_Scancode.SDL_SCANCODE_V; + + case InputKey.W: + return SDL_Scancode.SDL_SCANCODE_W; + + case InputKey.X: + return SDL_Scancode.SDL_SCANCODE_X; + + case InputKey.Y: + return SDL_Scancode.SDL_SCANCODE_Y; + + case InputKey.Z: + return SDL_Scancode.SDL_SCANCODE_Z; + + case InputKey.Number0: + return SDL_Scancode.SDL_SCANCODE_0; + + case InputKey.Number1: + return SDL_Scancode.SDL_SCANCODE_1; + + case InputKey.Number2: + return SDL_Scancode.SDL_SCANCODE_2; + + case InputKey.Number3: + return SDL_Scancode.SDL_SCANCODE_3; + + case InputKey.Number4: + return SDL_Scancode.SDL_SCANCODE_4; + + case InputKey.Number5: + return SDL_Scancode.SDL_SCANCODE_5; + + case InputKey.Number6: + return SDL_Scancode.SDL_SCANCODE_6; + + case InputKey.Number7: + return SDL_Scancode.SDL_SCANCODE_7; + + case InputKey.Number8: + return SDL_Scancode.SDL_SCANCODE_8; + + case InputKey.Number9: + return SDL_Scancode.SDL_SCANCODE_9; + + case InputKey.Grave: + return SDL_Scancode.SDL_SCANCODE_GRAVE; + + case InputKey.Minus: + return SDL_Scancode.SDL_SCANCODE_MINUS; + + case InputKey.Plus: + return SDL_Scancode.SDL_SCANCODE_EQUALS; + + case InputKey.BracketLeft: + return SDL_Scancode.SDL_SCANCODE_LEFTBRACKET; + + case InputKey.BracketRight: + return SDL_Scancode.SDL_SCANCODE_RIGHTBRACKET; + + case InputKey.Semicolon: + return SDL_Scancode.SDL_SCANCODE_SEMICOLON; + + case InputKey.Quote: + return SDL_Scancode.SDL_SCANCODE_APOSTROPHE; + + case InputKey.Comma: + return SDL_Scancode.SDL_SCANCODE_COMMA; + + case InputKey.Period: + return SDL_Scancode.SDL_SCANCODE_PERIOD; + + case InputKey.Slash: + return SDL_Scancode.SDL_SCANCODE_SLASH; + + case InputKey.BackSlash: + return SDL_Scancode.SDL_SCANCODE_BACKSLASH; + + case InputKey.NonUSBackSlash: + return SDL_Scancode.SDL_SCANCODE_NONUSBACKSLASH; + + case InputKey.Mute: + return SDL_Scancode.SDL_SCANCODE_AUDIOMUTE; + + case InputKey.PlayPause: + return SDL_Scancode.SDL_SCANCODE_AUDIOPLAY; + + case InputKey.Stop: + return SDL_Scancode.SDL_SCANCODE_AUDIOSTOP; + + case InputKey.VolumeUp: + return SDL_Scancode.SDL_SCANCODE_VOLUMEUP; + + case InputKey.VolumeDown: + return SDL_Scancode.SDL_SCANCODE_VOLUMEDOWN; + + case InputKey.TrackPrevious: + return SDL_Scancode.SDL_SCANCODE_AUDIOPREV; + + case InputKey.TrackNext: + return SDL_Scancode.SDL_SCANCODE_AUDIONEXT; + + case InputKey.LShift: + return SDL_Scancode.SDL_SCANCODE_LSHIFT; + + case InputKey.RShift: + return SDL_Scancode.SDL_SCANCODE_RSHIFT; + + case InputKey.LControl: + return SDL_Scancode.SDL_SCANCODE_LCTRL; + + case InputKey.RControl: + return SDL_Scancode.SDL_SCANCODE_RCTRL; + + case InputKey.LAlt: + return SDL_Scancode.SDL_SCANCODE_LALT; + + case InputKey.RAlt: + return SDL_Scancode.SDL_SCANCODE_RALT; + + case InputKey.LSuper: + return SDL_Scancode.SDL_SCANCODE_LGUI; + + case InputKey.RSuper: + return SDL_Scancode.SDL_SCANCODE_RGUI; + } + } + + public static WindowState ToWindowState(this SDL_WindowFlags windowFlags) + { + if (windowFlags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP) || + windowFlags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_BORDERLESS)) + return WindowState.FullscreenBorderless; + + if (windowFlags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MINIMIZED)) + return WindowState.Minimised; + + if (windowFlags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_FULLSCREEN)) + return WindowState.Fullscreen; + + if (windowFlags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MAXIMIZED)) + return WindowState.Maximised; + + return WindowState.Normal; + } + + public static SDL_WindowFlags ToFlags(this WindowState state) + { + switch (state) + { + case WindowState.Normal: + return 0; + + case WindowState.Fullscreen: + return SDL_WindowFlags.SDL_WINDOW_FULLSCREEN; + + case WindowState.Maximised: + return SDL_WindowFlags.SDL_WINDOW_MAXIMIZED; + + case WindowState.Minimised: + return SDL_WindowFlags.SDL_WINDOW_MINIMIZED; + + case WindowState.FullscreenBorderless: + return SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP; + } + + return 0; + } + + public static SDL_WindowFlags ToFlags(this GraphicsSurfaceType surfaceType) + { + switch (surfaceType) + { + case GraphicsSurfaceType.OpenGL: + return SDL_WindowFlags.SDL_WINDOW_OPENGL; + + case GraphicsSurfaceType.Vulkan when !RuntimeInfo.IsApple: + return SDL_WindowFlags.SDL_WINDOW_VULKAN; + + case GraphicsSurfaceType.Metal: + case GraphicsSurfaceType.Vulkan when RuntimeInfo.IsApple: + return SDL_WindowFlags.SDL_WINDOW_METAL; + } + + return 0; + } + + public static JoystickAxisSource ToJoystickAxisSource(this SDL_GameControllerAxis axis) + { + switch (axis) + { + default: + case SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_INVALID: + return 0; + + case SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX: + return JoystickAxisSource.GamePadLeftStickX; + + case SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY: + return JoystickAxisSource.GamePadLeftStickY; + + case SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERLEFT: + return JoystickAxisSource.GamePadLeftTrigger; + + case SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTX: + return JoystickAxisSource.GamePadRightStickX; + + case SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_RIGHTY: + return JoystickAxisSource.GamePadRightStickY; + + case SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + return JoystickAxisSource.GamePadRightTrigger; + } + } + + public static JoystickButton ToJoystickButton(this SDL_GameControllerButton button) + { + switch (button) + { + default: + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_INVALID: + return 0; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A: + return JoystickButton.GamePadA; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B: + return JoystickButton.GamePadB; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X: + return JoystickButton.GamePadX; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y: + return JoystickButton.GamePadY; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_BACK: + return JoystickButton.GamePadBack; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_GUIDE: + return JoystickButton.GamePadGuide; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START: + return JoystickButton.GamePadStart; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK: + return JoystickButton.GamePadLeftStick; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSTICK: + return JoystickButton.GamePadRightStick; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + return JoystickButton.GamePadLeftShoulder; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + return JoystickButton.GamePadRightShoulder; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_UP: + return JoystickButton.GamePadDPadUp; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_DOWN: + return JoystickButton.GamePadDPadDown; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_LEFT: + return JoystickButton.GamePadDPadLeft; + + case SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + return JoystickButton.GamePadDPadRight; + } + } + + public static SDL_Rect ToSDLRect(this RectangleI rectangle) => + new SDL_Rect + { + x = rectangle.X, + y = rectangle.Y, + h = rectangle.Height, + w = rectangle.Width, + }; + + /// + /// Converts a UTF-8 byte pointer to a string. + /// + /// Most commonly used with SDL text events. + /// Pointer to UTF-8 encoded byte array. + /// The resulting string + /// true if the was successfully converted to a string. + public static unsafe bool TryGetStringFromBytePointer(byte* bytePointer, out string str) + { + IntPtr ptr = new IntPtr(bytePointer); + + if (ptr == IntPtr.Zero) + { + str = null; + return false; + } + + str = Marshal.PtrToStringUTF8(ptr) ?? string.Empty; + return true; + } + + public static DisplayMode ToDisplayMode(this SDL_DisplayMode mode, int displayIndex) + { + SDL_PixelFormatEnumToMasks(mode.format, out int bpp, out _, out _, out _, out _); + return new DisplayMode(SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex); + } + + public static string ReadableName(this SDL_LogCategory category) + { + switch (category) + { + case SDL_LogCategory.SDL_LOG_CATEGORY_APPLICATION: + return "application"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_ERROR: + return "error"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_ASSERT: + return "assert"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_SYSTEM: + return "system"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_AUDIO: + return "audio"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_VIDEO: + return "video"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_RENDER: + return "render"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_INPUT: + return "input"; + + case SDL_LogCategory.SDL_LOG_CATEGORY_TEST: + return "test"; + + default: + return "unknown"; + } + } + + public static string ReadableName(this SDL_LogPriority priority) + { + switch (priority) + { + case SDL_LogPriority.SDL_LOG_PRIORITY_VERBOSE: + return "verbose"; + + case SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG: + return "debug"; + + case SDL_LogPriority.SDL_LOG_PRIORITY_INFO: + return "info"; + + case SDL_LogPriority.SDL_LOG_PRIORITY_WARN: + return "warn"; + + case SDL_LogPriority.SDL_LOG_PRIORITY_ERROR: + return "error"; + + case SDL_LogPriority.SDL_LOG_PRIORITY_CRITICAL: + return "critical"; + + default: + return "unknown"; + } + } + + /// + /// Gets the readable string for this . + /// + /// + /// string in the format of 1920x1080@60. + /// + public static string ReadableString(this SDL_DisplayMode mode) => $"{mode.w}x{mode.h}@{mode.refresh_rate}"; + + /// + /// Gets the SDL error, and then clears it. + /// + public static string GetAndClearError() + { + string error = SDL_GetError(); + SDL_ClearError(); + return error; + } + + private static bool tryGetTouchDeviceIndex(long touchId, out int index) + { + int n = SDL_GetNumTouchDevices(); + + for (int i = 0; i < n; i++) + { + long currentTouchId = SDL_GetTouchDevice(i); + + if (touchId == currentTouchId) + { + index = i; + return true; + } + } + + index = -1; + return false; + } + + /// + /// Gets the of the touch device for this . + /// + /// + /// On Windows, this will return "touch" for touchscreen events or "pen" for pen/tablet events. + /// + public static bool TryGetTouchName(this SDL_TouchFingerEvent e, out string name) + { + if (tryGetTouchDeviceIndex(e.touchId, out int index)) + { + name = SDL_GetTouchName(index); + return name != null; + } + + name = null; + return false; + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2GraphicsSurface.cs b/osu.Framework/Platform/SDL2/SDL2GraphicsSurface.cs new file mode 100644 index 0000000000..c0f5e0b7d9 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2GraphicsSurface.cs @@ -0,0 +1,216 @@ +// 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.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using osuTK.Graphics; +using osuTK.Graphics.ES30; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + internal class SDL2GraphicsSurface : IGraphicsSurface, IOpenGLGraphicsSurface, IMetalGraphicsSurface, ILinuxGraphicsSurface + { + private readonly SDL2Window window; + + private IntPtr context; + + public IntPtr WindowHandle => window.WindowHandle; + public IntPtr DisplayHandle => window.DisplayHandle; + + public GraphicsSurfaceType Type { get; } + + public SDL2GraphicsSurface(SDL2Window window, GraphicsSurfaceType surfaceType) + { + this.window = window; + Type = surfaceType; + + switch (surfaceType) + { + case GraphicsSurfaceType.OpenGL: + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ACCUM_ALPHA_SIZE, 0); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DEPTH_SIZE, 16); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STENCIL_SIZE, 8); + break; + + case GraphicsSurfaceType.Vulkan: + case GraphicsSurfaceType.Metal: + case GraphicsSurfaceType.Direct3D11: + break; + + default: + throw new ArgumentException($"Unexpected graphics surface: {Type}.", nameof(surfaceType)); + } + } + + public void Initialise() + { + if (Type == GraphicsSurfaceType.OpenGL) + initialiseOpenGL(); + } + + public Size GetDrawableSize() + { + SDL_GetWindowSizeInPixels(window.SDLWindowHandle, out int width, out int height); + return new Size(width, height); + } + + #region OpenGL-specific implementation + + private void initialiseOpenGL() + { + if (RuntimeInfo.IsMobile) + { + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_ES); + + // Minimum OpenGL version for ES profile: + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 0); + } + else + { + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_CORE); + + // Minimum OpenGL version for core profile: + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 2); + } + + context = SDL_GL_CreateContext(window.SDLWindowHandle); + + if (context == IntPtr.Zero) + throw new InvalidOperationException($"Failed to create an SDL2 GL context ({SDL_GetError()})"); + + SDL_GL_MakeCurrent(window.SDLWindowHandle, context); + + loadBindings(); + } + + private void loadBindings() + { + loadEntryPoints(new osuTK.Graphics.OpenGL.GL()); + loadEntryPoints(new osuTK.Graphics.OpenGL4.GL()); + loadEntryPoints(new osuTK.Graphics.ES11.GL()); + loadEntryPoints(new osuTK.Graphics.ES20.GL()); + loadEntryPoints(new GL()); + } + + private unsafe void loadEntryPoints(GraphicsBindingsBase bindings) + { + var type = bindings.GetType(); + var pointsInfo = type.GetRuntimeFields().First(x => x.Name == "_EntryPointsInstance"); + var namesInfo = type.GetRuntimeFields().First(x => x.Name == "_EntryPointNamesInstance"); + var offsetsInfo = type.GetRuntimeFields().First(x => x.Name == "_EntryPointNameOffsetsInstance"); + + IntPtr[]? entryPointsInstance = (IntPtr[]?)pointsInfo.GetValue(bindings); + byte[]? entryPointNamesInstance = (byte[]?)namesInfo.GetValue(bindings); + int[]? entryPointNameOffsetsInstance = (int[]?)offsetsInfo.GetValue(bindings); + + Debug.Assert(entryPointsInstance != null); + Debug.Assert(entryPointNameOffsetsInstance != null); + + fixed (byte* name = entryPointNamesInstance) + { + for (int i = 0; i < entryPointsInstance.Length; i++) + { + byte* ptr = name + entryPointNameOffsetsInstance[i]; + string? str = Marshal.PtrToStringAnsi(new IntPtr(ptr)); + + Debug.Assert(str != null); + entryPointsInstance[i] = getProcAddress(str); + } + } + + pointsInfo.SetValue(bindings, entryPointsInstance); + } + + private IntPtr getProcAddress(string symbol) + { + const int error_category = (int)SDL_LogCategory.SDL_LOG_CATEGORY_ERROR; + SDL_LogPriority oldPriority = SDL_LogGetPriority(error_category); + + // Prevent logging calls to SDL_GL_GetProcAddress() that fail on systems which don't have the requested symbol (typically macOS). + SDL_LogSetPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO); + + IntPtr ret = SDL_GL_GetProcAddress(symbol); + + // Reset the logging behaviour. + SDL_LogSetPriority(error_category, oldPriority); + + return ret; + } + + int? IOpenGLGraphicsSurface.BackbufferFramebuffer + { + get + { + if (window.SDLWindowHandle == IntPtr.Zero) + return null; + + var wmInfo = window.GetWindowSystemInformation(); + + switch (wmInfo.subsystem) + { + case SDL_SYSWM_TYPE.SDL_SYSWM_UIKIT: + return (int)wmInfo.info.uikit.framebuffer; + } + + return null; + } + } + + // cache value locally as requesting from SDL is not free. + // it is assumed that we are the only thing changing vsync modes. + private bool? verticalSync; + + bool IOpenGLGraphicsSurface.VerticalSync + { + get + { + if (verticalSync != null) + return verticalSync.Value; + + return (verticalSync = SDL_GL_GetSwapInterval() != 0).Value; + } + set + { + if (RuntimeInfo.IsDesktop) + { + SDL_GL_SetSwapInterval(value ? 1 : 0); + verticalSync = value; + } + } + } + + IntPtr IOpenGLGraphicsSurface.WindowContext => context; + IntPtr IOpenGLGraphicsSurface.CurrentContext => SDL_GL_GetCurrentContext(); + + void IOpenGLGraphicsSurface.SwapBuffers() => SDL_GL_SwapWindow(window.SDLWindowHandle); + void IOpenGLGraphicsSurface.CreateContext() => SDL_GL_CreateContext(window.SDLWindowHandle); + void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL_GL_DeleteContext(context); + void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL_GL_MakeCurrent(window.SDLWindowHandle, context); + void IOpenGLGraphicsSurface.ClearCurrent() => SDL_GL_MakeCurrent(window.SDLWindowHandle, IntPtr.Zero); + IntPtr IOpenGLGraphicsSurface.GetProcAddress(string symbol) => getProcAddress(symbol); + + #endregion + + #region Metal-specific implementation + + IntPtr IMetalGraphicsSurface.CreateMetalView() => SDL_Metal_CreateView(window.SDLWindowHandle); + + #endregion + + #region Linux-specific implementation + + bool ILinuxGraphicsSurface.IsWayland => window.IsWayland; + + #endregion + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs b/osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs new file mode 100644 index 0000000000..a4f0098278 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs @@ -0,0 +1,230 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Globalization; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + public class SDL2ReadableKeyCombinationProvider : ReadableKeyCombinationProvider + { + protected override string GetReadableKey(InputKey key) + { + var keycode = SDL_GetKeyFromScancode(key.ToScancode()); + + // early return if unknown. probably because key isn't a keyboard key, or doesn't map to an `SDL_Scancode`. + if (keycode == SDL_Keycode.SDLK_UNKNOWN) + return base.GetReadableKey(key); + + string name; + + // overrides for some keys that we want displayed differently from SDL_GetKeyName(). + if (TryGetNameFromKeycode(keycode, out name)) + return name; + + name = SDL_GetKeyName(keycode); + + // fall back if SDL couldn't find a name. + if (string.IsNullOrEmpty(name)) + return base.GetReadableKey(key); + + // true if SDL_GetKeyName() returned a proper key/scancode name. + // see https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/events/SDL_keyboard.c#L1012 + if (((int)keycode & SDLK_SCANCODE_MASK) != 0) + return name; + + // SDL_GetKeyName() returned a unicode character that would be produced if that key was pressed. + // consumers expect an uppercase letter. + // `.ToUpper()` with current culture may be slightly inaccurate if the framework locale + // is different from the locale of the keyboard layout being used, + // but we have no means to detect this anyway, as SDL doesn't provide the name of the layout. + return name.ToUpper(CultureInfo.CurrentCulture); + } + + /// + /// Provides overrides for some keys that we want displayed differently from SDL_GetKeyName(). + /// + /// + /// Should be overriden per-platform to provide platform-specific names for applicable keys. + /// + protected virtual bool TryGetNameFromKeycode(SDL_Keycode keycode, out string name) + { + switch (keycode) + { + case SDL_Keycode.SDLK_RETURN: + name = "Enter"; + return true; + + case SDL_Keycode.SDLK_ESCAPE: + name = "Esc"; + return true; + + case SDL_Keycode.SDLK_BACKSPACE: + name = "Backsp"; + return true; + + case SDL_Keycode.SDLK_TAB: + name = "Tab"; + return true; + + case SDL_Keycode.SDLK_SPACE: + name = "Space"; + return true; + + case SDL_Keycode.SDLK_PLUS: + name = "Plus"; + return true; + + case SDL_Keycode.SDLK_MINUS: + name = "Minus"; + return true; + + case SDL_Keycode.SDLK_DELETE: + name = "Del"; + return true; + + case SDL_Keycode.SDLK_CAPSLOCK: + name = "Caps"; + return true; + + case SDL_Keycode.SDLK_INSERT: + name = "Ins"; + return true; + + case SDL_Keycode.SDLK_PAGEUP: + name = "PgUp"; + return true; + + case SDL_Keycode.SDLK_PAGEDOWN: + name = "PgDn"; + return true; + + case SDL_Keycode.SDLK_NUMLOCKCLEAR: + name = "NumLock"; + return true; + + case SDL_Keycode.SDLK_KP_DIVIDE: + name = "NumpadDivide"; + return true; + + case SDL_Keycode.SDLK_KP_MULTIPLY: + name = "NumpadMultiply"; + return true; + + case SDL_Keycode.SDLK_KP_MINUS: + name = "NumpadMinus"; + return true; + + case SDL_Keycode.SDLK_KP_PLUS: + name = "NumpadPlus"; + return true; + + case SDL_Keycode.SDLK_KP_ENTER: + name = "NumpadEnter"; + return true; + + case SDL_Keycode.SDLK_KP_PERIOD: + name = "NumpadDecimal"; + return true; + + case SDL_Keycode.SDLK_KP_0: + name = "Numpad0"; + return true; + + case SDL_Keycode.SDLK_KP_1: + name = "Numpad1"; + return true; + + case SDL_Keycode.SDLK_KP_2: + name = "Numpad2"; + return true; + + case SDL_Keycode.SDLK_KP_3: + name = "Numpad3"; + return true; + + case SDL_Keycode.SDLK_KP_4: + name = "Numpad4"; + return true; + + case SDL_Keycode.SDLK_KP_5: + name = "Numpad5"; + return true; + + case SDL_Keycode.SDLK_KP_6: + name = "Numpad6"; + return true; + + case SDL_Keycode.SDLK_KP_7: + name = "Numpad7"; + return true; + + case SDL_Keycode.SDLK_KP_8: + name = "Numpad8"; + return true; + + case SDL_Keycode.SDLK_KP_9: + name = "Numpad9"; + return true; + + case SDL_Keycode.SDLK_LCTRL: + name = "LCtrl"; + return true; + + case SDL_Keycode.SDLK_LSHIFT: + name = "LShift"; + return true; + + case SDL_Keycode.SDLK_LALT: + name = "LAlt"; + return true; + + case SDL_Keycode.SDLK_RCTRL: + name = "RCtrl"; + return true; + + case SDL_Keycode.SDLK_RSHIFT: + name = "RShift"; + return true; + + case SDL_Keycode.SDLK_RALT: + name = "RAlt"; + return true; + + case SDL_Keycode.SDLK_VOLUMEUP: + name = "Vol. Up"; + return true; + + case SDL_Keycode.SDLK_VOLUMEDOWN: + name = "Vol. Down"; + return true; + + case SDL_Keycode.SDLK_AUDIONEXT: + name = "Media Next"; + return true; + + case SDL_Keycode.SDLK_AUDIOPREV: + name = "Media Previous"; + return true; + + case SDL_Keycode.SDLK_AUDIOSTOP: + name = "Media Stop"; + return true; + + case SDL_Keycode.SDLK_AUDIOPLAY: + name = "Media Play"; + return true; + + case SDL_Keycode.SDLK_AUDIOMUTE: + name = "Mute"; + return true; + + default: + name = string.Empty; + return false; + } + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2Structs.cs b/osu.Framework/Platform/SDL2/SDL2Structs.cs new file mode 100644 index 0000000000..3e9f4f5b37 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2Structs.cs @@ -0,0 +1,50 @@ +// 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.Runtime.InteropServices; +using static SDL2.SDL; + +// ReSharper disable MemberCanBePrivate.Global +// (Some members not currently used) + +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +// (Mimics SDL and SDL2-CS naming) + +#pragma warning disable IDE1006 // Naming style + +namespace osu.Framework.Platform.SDL2 +{ + internal static class SDL2Structs + { + [StructLayout(LayoutKind.Sequential)] + internal struct INTERNAL_windows_wmmsg + { + public IntPtr hwnd; + public uint msg; + public ulong wParam; + public long lParam; + } + + [StructLayout(LayoutKind.Explicit)] + internal struct INTERNAL_SysWMmsgUnion + { + [FieldOffset(0)] + public INTERNAL_windows_wmmsg win; + + // could add more native events here if required + } + + /// + /// Member msg of . + /// + [StructLayout(LayoutKind.Sequential)] + public struct SDL_SysWMmsg + { + public SDL_version version; + public SDL_SYSWM_TYPE subsystem; + public INTERNAL_SysWMmsgUnion msg; + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2Window.cs b/osu.Framework/Platform/SDL2/SDL2Window.cs new file mode 100644 index 0000000000..3b6564993c --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2Window.cs @@ -0,0 +1,680 @@ +// 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.IO; +using System.Runtime.InteropServices; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Configuration; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Extensions.ImageExtensions; +using osu.Framework.Logging; +using osu.Framework.Threading; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Image = SixLabors.ImageSharp.Image; +using Point = System.Drawing.Point; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + /// + /// Default implementation of a window, using SDL for windowing and graphics support. + /// + internal abstract partial class SDL2Window : IWindow + { + internal IntPtr SDLWindowHandle { get; private set; } = IntPtr.Zero; + + private readonly SDL2GraphicsSurface graphicsSurface; + IGraphicsSurface IWindow.GraphicsSurface => graphicsSurface; + + /// + /// Returns true if window has been created. + /// Returns false if the window has not yet been created, or has been closed. + /// + public bool Exists { get; private set; } + + public BindableSafeArea SafeAreaPadding { get; } = new BindableSafeArea(); + + public virtual Point PointToClient(Point point) => point; + + public virtual Point PointToScreen(Point point) => point; + + private const int default_width = 1366; + private const int default_height = 768; + + private const int default_icon_size = 256; + + /// + /// Scheduler for actions to run before the next event loop. + /// + private readonly Scheduler commandScheduler = new Scheduler(); + + /// + /// Scheduler for actions to run at the end of the current event loop. + /// + protected readonly Scheduler EventScheduler = new Scheduler(); + + private string title = string.Empty; + + /// + /// Gets and sets the window title. + /// + public string Title + { + get => title; + set + { + title = value; + ScheduleCommand(() => SDL_SetWindowTitle(SDLWindowHandle, title)); + } + } + + /// + /// Whether the current display server is Wayland. + /// + internal bool IsWayland + { + get + { + if (SDLWindowHandle == IntPtr.Zero) + return false; + + return GetWindowSystemInformation().subsystem == SDL_SYSWM_TYPE.SDL_SYSWM_WAYLAND; + } + } + + /// + /// Gets the native window handle as provided by the operating system. + /// + public IntPtr WindowHandle + { + get + { + if (SDLWindowHandle == IntPtr.Zero) + return IntPtr.Zero; + + var wmInfo = GetWindowSystemInformation(); + + // Window handle is selected per subsystem as defined at: + // https://wiki.libsdl.org/SDL_SysWMinfo + switch (wmInfo.subsystem) + { + case SDL_SYSWM_TYPE.SDL_SYSWM_WINDOWS: + return wmInfo.info.win.window; + + case SDL_SYSWM_TYPE.SDL_SYSWM_X11: + return wmInfo.info.x11.window; + + case SDL_SYSWM_TYPE.SDL_SYSWM_DIRECTFB: + return wmInfo.info.dfb.window; + + case SDL_SYSWM_TYPE.SDL_SYSWM_COCOA: + return wmInfo.info.cocoa.window; + + case SDL_SYSWM_TYPE.SDL_SYSWM_UIKIT: + return wmInfo.info.uikit.window; + + case SDL_SYSWM_TYPE.SDL_SYSWM_WAYLAND: + return wmInfo.info.wl.surface; + + case SDL_SYSWM_TYPE.SDL_SYSWM_ANDROID: + return wmInfo.info.android.window; + + default: + return IntPtr.Zero; + } + } + } + + public IntPtr DisplayHandle + { + get + { + if (SDLWindowHandle == IntPtr.Zero) + return IntPtr.Zero; + + var wmInfo = GetWindowSystemInformation(); + + switch (wmInfo.subsystem) + { + case SDL_SYSWM_TYPE.SDL_SYSWM_X11: + return wmInfo.info.x11.display; + + case SDL_SYSWM_TYPE.SDL_SYSWM_WAYLAND: + return wmInfo.info.wl.display; + + default: + return IntPtr.Zero; + } + } + } + + internal SDL_SysWMinfo GetWindowSystemInformation() + { + if (SDLWindowHandle == IntPtr.Zero) + return default; + + var wmInfo = new SDL_SysWMinfo(); + SDL_GetVersion(out wmInfo.version); + SDL_GetWindowWMInfo(SDLWindowHandle, ref wmInfo); + return wmInfo; + } + + public bool CapsLockPressed => SDL_GetModState().HasFlagFast(SDL_Keymod.KMOD_CAPS); + + // references must be kept to avoid GC, see https://stackoverflow.com/a/6193914 + + [UsedImplicitly] + private SDL_LogOutputFunction logOutputDelegate; + + [UsedImplicitly] + private SDL_EventFilter? eventFilterDelegate; + + [UsedImplicitly] + private SDL_EventFilter? eventWatchDelegate; + + /// + /// Represents a handle to this instance, used for unmanaged callbacks. + /// + protected ObjectHandle ObjectHandle { get; private set; } + + protected SDL2Window(GraphicsSurfaceType surfaceType) + { + ObjectHandle = new ObjectHandle(this, GCHandleType.Normal); + + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) + { + throw new InvalidOperationException($"Failed to initialise SDL: {SDL_GetError()}"); + } + + SDL_LogSetPriority((int)SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG); + SDL_LogSetOutputFunction(logOutputDelegate = logOutput, IntPtr.Zero); + + graphicsSurface = new SDL2GraphicsSurface(this, surfaceType); + + CursorStateBindable.ValueChanged += evt => + { + updateCursorVisibility(!evt.NewValue.HasFlagFast(CursorState.Hidden)); + updateCursorConfinement(); + }; + + populateJoysticks(); + } + + [MonoPInvokeCallback(typeof(SDL_LogOutputFunction))] + private static void logOutput(IntPtr _, int categoryInt, SDL_LogPriority priority, IntPtr messagePtr) + { + var category = (SDL_LogCategory)categoryInt; + string? message = Marshal.PtrToStringUTF8(messagePtr); + + Logger.Log($@"SDL {category.ReadableName()} log [{priority.ReadableName()}]: {message}"); + } + + public void SetupWindow(FrameworkConfigManager config) + { + setupWindowing(config); + setupInput(config); + } + + public virtual void Create() + { + SDL_WindowFlags flags = SDL_WindowFlags.SDL_WINDOW_RESIZABLE | + SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI | + SDL_WindowFlags.SDL_WINDOW_HIDDEN; // shown after first swap to avoid white flash on startup (windows) + + flags |= WindowState.ToFlags(); + flags |= graphicsSurface.Type.ToFlags(); + + SDL_SetHint(SDL_HINT_WINDOWS_NO_CLOSE_ON_ALT_F4, "1"); + SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1"); + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_CENTER, "0"); + SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); // disable touch events generating synthetic mouse events on desktop platforms + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); // disable mouse events generating synthetic touch events on mobile platforms + + // we want text input to only be active when SDL2DesktopWindowTextInput is active. + // SDL activates it by default on some platforms: https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/video/SDL_video.c#L573-L582 + // so we deactivate it on startup. + SDL_StopTextInput(); + + SDLWindowHandle = SDL_CreateWindow(title, Position.X, Position.Y, Size.Width, Size.Height, flags); + + if (SDLWindowHandle == IntPtr.Zero) + throw new InvalidOperationException($"Failed to create SDL window. SDL Error: {SDL_GetError()}"); + + graphicsSurface.Initialise(); + + initialiseWindowingAfterCreation(); + Exists = true; + } + + /// + /// Starts the window's run loop. + /// + public void Run() + { + SDL_SetEventFilter(eventFilterDelegate = eventFilter, ObjectHandle.Handle); + SDL_AddEventWatch(eventWatchDelegate = eventWatch, ObjectHandle.Handle); + + RunMainLoop(); + } + + /// + /// Runs the main window loop. + /// + /// + /// By default this will block and indefinitely call as long as the window . + /// Once the main loop finished running, cleanup logic will run. + /// + /// This may be overridden for special use cases, like mobile platforms which delegate execution of frames to the OS + /// and don't require any kind of exit logic to exist. + /// + protected virtual void RunMainLoop() + { + while (Exists) + RunFrame(); + + Exited?.Invoke(); + Close(); + SDL_Quit(); + } + + /// + /// Run a single frame. + /// + protected void RunFrame() + { + commandScheduler.Update(); + + if (!Exists) + return; + + if (pendingWindowState != null) + updateAndFetchWindowSpecifics(); + + pollSDLEvents(); + + if (!cursorInWindow.Value) + pollMouse(); + + EventScheduler.Update(); + Update?.Invoke(); + } + + /// + /// Handles s fired from the SDL event filter. + /// + /// + /// As per SDL's recommendation, application events should always be handled via the event filter. + /// See: https://wiki.libsdl.org/SDL2/SDL_EventType#android_ios_and_winrt_events + /// + protected virtual void HandleEventFromFilter(SDL_Event evt) + { + switch (evt.type) + { + case SDL_EventType.SDL_APP_TERMINATING: + handleQuitEvent(evt.quit); + break; + + case SDL_EventType.SDL_APP_DIDENTERBACKGROUND: + Suspended?.Invoke(); + break; + + case SDL_EventType.SDL_APP_WILLENTERFOREGROUND: + Resumed?.Invoke(); + break; + + case SDL_EventType.SDL_APP_LOWMEMORY: + LowOnMemory?.Invoke(); + break; + } + } + + protected void HandleEventFromWatch(SDL_Event evt) + { + switch (evt.type) + { + case SDL_EventType.SDL_WINDOWEVENT: + // polling via SDL_PollEvent blocks on resizes (https://stackoverflow.com/a/50858339) + if (evt.window.windowEvent == SDL_WindowEventID.SDL_WINDOWEVENT_RESIZED && !updatingWindowStateAndSize) + fetchWindowSize(); + + break; + } + } + + [MonoPInvokeCallback(typeof(SDL_EventFilter))] + private static int eventFilter(IntPtr userdata, IntPtr eventPtr) + { + var handle = new ObjectHandle(userdata); + if (handle.GetTarget(out SDL2Window window)) + window.HandleEventFromFilter(Marshal.PtrToStructure(eventPtr)); + + return 1; + } + + [MonoPInvokeCallback(typeof(SDL_EventFilter))] + private static int eventWatch(IntPtr userdata, IntPtr eventPtr) + { + var handle = new ObjectHandle(userdata); + if (handle.GetTarget(out SDL2Window window)) + window.HandleEventFromWatch(Marshal.PtrToStructure(eventPtr)); + + return 1; + } + + private bool firstDraw = true; + + public void OnDraw() + { + if (!firstDraw) + return; + + Visible = true; + firstDraw = false; + } + + /// + /// Forcefully closes the window. + /// + public void Close() + { + if (Exists) + { + // Close will be called as part of finishing the Run loop. + ScheduleCommand(() => Exists = false); + } + else + { + if (SDLWindowHandle != IntPtr.Zero) + { + SDL_DestroyWindow(SDLWindowHandle); + SDLWindowHandle = IntPtr.Zero; + } + } + } + + public void Raise() => ScheduleCommand(() => + { + var flags = (SDL_WindowFlags)SDL_GetWindowFlags(SDLWindowHandle); + + if (flags.HasFlagFast(SDL_WindowFlags.SDL_WINDOW_MINIMIZED)) + SDL_RestoreWindow(SDLWindowHandle); + + SDL_RaiseWindow(SDLWindowHandle); + }); + + public void Hide() => ScheduleCommand(() => + { + SDL_HideWindow(SDLWindowHandle); + }); + + public void Show() => ScheduleCommand(() => + { + SDL_ShowWindow(SDLWindowHandle); + }); + + public void Flash(bool flashUntilFocused = false) => ScheduleCommand(() => + { + if (isActive.Value) + return; + + if (!RuntimeInfo.IsDesktop) + return; + + SDL_FlashWindow(SDLWindowHandle, flashUntilFocused + ? SDL_FlashOperation.SDL_FLASH_UNTIL_FOCUSED + : SDL_FlashOperation.SDL_FLASH_BRIEFLY); + }); + + public void CancelFlash() => ScheduleCommand(() => + { + if (!RuntimeInfo.IsDesktop) + return; + + SDL_FlashWindow(SDLWindowHandle, SDL_FlashOperation.SDL_FLASH_CANCEL); + }); + + public void EnableScreenSuspension() => ScheduleCommand(SDL_EnableScreenSaver); + + public void DisableScreenSuspension() => ScheduleCommand(SDL_DisableScreenSaver); + + /// + /// Attempts to set the window's icon to the specified image. + /// + /// An to set as the window icon. + private unsafe void setSDLIcon(Image image) + { + var pixelMemory = image.CreateReadOnlyPixelMemory(); + var imageSize = image.Size; + + ScheduleCommand(() => + { + var pixelSpan = pixelMemory.Span; + + IntPtr surface; + fixed (Rgba32* ptr = pixelSpan) + surface = SDL_CreateRGBSurfaceFrom(new IntPtr(ptr), imageSize.Width, imageSize.Height, 32, imageSize.Width * 4, 0xff, 0xff00, 0xff0000, 0xff000000); + + SDL_SetWindowIcon(SDLWindowHandle, surface); + SDL_FreeSurface(surface); + }); + } + + #region SDL Event Handling + + /// + /// Adds an to the expected to handle event callbacks. + /// + /// The to execute. + protected void ScheduleEvent(Action action) => EventScheduler.Add(action, false); + + protected void ScheduleCommand(Action action) => commandScheduler.Add(action, false); + + private const int events_per_peep = 64; + private readonly SDL_Event[] events = new SDL_Event[events_per_peep]; + + /// + /// Poll for all pending events. + /// + private void pollSDLEvents() + { + SDL_PumpEvents(); + + int eventsRead; + + do + { + eventsRead = SDL_PeepEvents(events, events_per_peep, SDL_eventaction.SDL_GETEVENT, SDL_EventType.SDL_FIRSTEVENT, SDL_EventType.SDL_LASTEVENT); + for (int i = 0; i < eventsRead; i++) + HandleEvent(events[i]); + } while (eventsRead == events_per_peep); + } + + /// + /// Handles s polled on the main thread. + /// + protected virtual void HandleEvent(SDL_Event e) + { + switch (e.type) + { + case SDL_EventType.SDL_QUIT: + handleQuitEvent(e.quit); + break; + + case SDL_EventType.SDL_DISPLAYEVENT: + handleDisplayEvent(e.display); + break; + + case SDL_EventType.SDL_WINDOWEVENT: + handleWindowEvent(e.window); + break; + + case SDL_EventType.SDL_KEYDOWN: + case SDL_EventType.SDL_KEYUP: + handleKeyboardEvent(e.key); + break; + + case SDL_EventType.SDL_TEXTEDITING: + HandleTextEditingEvent(e.edit); + break; + + case SDL_EventType.SDL_TEXTINPUT: + HandleTextInputEvent(e.text); + break; + + case SDL_EventType.SDL_KEYMAPCHANGED: + handleKeymapChangedEvent(); + break; + + case SDL_EventType.SDL_MOUSEMOTION: + handleMouseMotionEvent(e.motion); + break; + + case SDL_EventType.SDL_MOUSEBUTTONDOWN: + case SDL_EventType.SDL_MOUSEBUTTONUP: + handleMouseButtonEvent(e.button); + break; + + case SDL_EventType.SDL_MOUSEWHEEL: + handleMouseWheelEvent(e.wheel); + break; + + case SDL_EventType.SDL_JOYAXISMOTION: + handleJoyAxisEvent(e.jaxis); + break; + + case SDL_EventType.SDL_JOYBALLMOTION: + handleJoyBallEvent(e.jball); + break; + + case SDL_EventType.SDL_JOYHATMOTION: + handleJoyHatEvent(e.jhat); + break; + + case SDL_EventType.SDL_JOYBUTTONDOWN: + case SDL_EventType.SDL_JOYBUTTONUP: + handleJoyButtonEvent(e.jbutton); + break; + + case SDL_EventType.SDL_JOYDEVICEADDED: + case SDL_EventType.SDL_JOYDEVICEREMOVED: + handleJoyDeviceEvent(e.jdevice); + break; + + case SDL_EventType.SDL_CONTROLLERAXISMOTION: + handleControllerAxisEvent(e.caxis); + break; + + case SDL_EventType.SDL_CONTROLLERBUTTONDOWN: + case SDL_EventType.SDL_CONTROLLERBUTTONUP: + handleControllerButtonEvent(e.cbutton); + break; + + case SDL_EventType.SDL_CONTROLLERDEVICEADDED: + case SDL_EventType.SDL_CONTROLLERDEVICEREMOVED: + case SDL_EventType.SDL_CONTROLLERDEVICEREMAPPED: + handleControllerDeviceEvent(e.cdevice); + break; + + case SDL_EventType.SDL_FINGERDOWN: + case SDL_EventType.SDL_FINGERUP: + case SDL_EventType.SDL_FINGERMOTION: + HandleTouchFingerEvent(e.tfinger); + break; + + case SDL_EventType.SDL_DROPFILE: + case SDL_EventType.SDL_DROPTEXT: + case SDL_EventType.SDL_DROPBEGIN: + case SDL_EventType.SDL_DROPCOMPLETE: + handleDropEvent(e.drop); + break; + } + } + + // ReSharper disable once UnusedParameter.Local + private void handleQuitEvent(SDL_QuitEvent evtQuit) => ExitRequested?.Invoke(); + + #endregion + + public void SetIconFromStream(Stream imageStream) + { + using (var ms = new MemoryStream()) + { + imageStream.CopyTo(ms); + ms.Position = 0; + + try + { + SetIconFromImage(Image.Load(ms.GetBuffer())); + } + catch + { + if (IconGroup.TryParse(ms.GetBuffer(), out var iconGroup)) + SetIconFromGroup(iconGroup); + } + } + } + + internal virtual void SetIconFromGroup(IconGroup iconGroup) + { + // LoadRawIcon returns raw PNG data if available, which avoids any Windows-specific pinvokes + byte[]? bytes = iconGroup.LoadRawIcon(default_icon_size, default_icon_size); + if (bytes == null) + return; + + SetIconFromImage(Image.Load(bytes)); + } + + internal virtual void SetIconFromImage(Image iconImage) => setSDLIcon(iconImage); + + #region Events + + /// + /// Invoked once every window event loop. + /// + public event Action? Update; + + /// + /// Invoked when the application associated with this has been suspended. + /// + public event Action? Suspended; + + /// + /// Invoked when the application associated with this has been resumed from suspension. + /// + public event Action? Resumed; + + /// + /// Invoked when the operating system is low on memory, in order for the application to free some. + /// + public event Action? LowOnMemory; + + /// + /// Invoked when the window close (X) button or another platform-native exit action has been pressed. + /// + public event Action? ExitRequested; + + /// + /// Invoked when the window is about to close. + /// + public event Action? Exited; + + /// + /// Invoked when the user drops a file into the window. + /// + public event Action? DragDrop; + + #endregion + + public void Dispose() + { + Close(); + SDL_Quit(); + + ObjectHandle.Dispose(); + } + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs new file mode 100644 index 0000000000..2764dccef7 --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs @@ -0,0 +1,665 @@ +// 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.Diagnostics; +using System.Drawing; +using osu.Framework.Bindables; +using osu.Framework.Configuration; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Input; +using osu.Framework.Input.States; +using osu.Framework.Logging; +using osuTK; +using osuTK.Input; +using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + internal partial class SDL2Window + { + private void setupInput(FrameworkConfigManager config) + { + config.BindWith(FrameworkSetting.ConfineMouseMode, ConfineMouseMode); + + WindowMode.BindValueChanged(_ => updateConfineMode()); + ConfineMouseMode.BindValueChanged(_ => updateConfineMode()); + } + + private bool relativeMouseMode; + + /// + /// Set the state of SDL2's RelativeMouseMode (https://wiki.libsdl.org/SDL_SetRelativeMouseMode). + /// On all platforms, this will lock the mouse to the window (although escaping by setting is still possible via a local implementation). + /// On windows, this will use raw input if available. + /// + public bool RelativeMouseMode + { + get => relativeMouseMode; + set + { + if (relativeMouseMode == value) + return; + + if (value && !CursorState.HasFlagFast(CursorState.Hidden)) + throw new InvalidOperationException($"Cannot set {nameof(RelativeMouseMode)} to true when the cursor is not hidden via {nameof(CursorState)}."); + + relativeMouseMode = value; + ScheduleCommand(() => SDL_SetRelativeMouseMode(value ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); + updateCursorConfinement(); + } + } + + /// + /// Controls whether the mouse is automatically captured when buttons are pressed and the cursor is outside the window. + /// Only works with disabled. + /// + /// + /// If the cursor leaves the window while it's captured, is not sent until the button(s) are released. + /// And if the cursor leaves and enters the window while captured, is not sent either. + /// We disable relative mode when the cursor exits window bounds (not on the event), but we only enable it again on . + /// The above culminate in staying off when the cursor leaves and enters the window bounds when any buttons are pressed. + /// This is an invalid state, as the cursor is inside the window, and is off. + /// + internal bool MouseAutoCapture + { + set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1" : "0")); + } + + /// + /// Provides a bindable that controls the window's . + /// + public Bindable CursorStateBindable { get; } = new Bindable(); + + public CursorState CursorState + { + get => CursorStateBindable.Value; + set => CursorStateBindable.Value = value; + } + + private RectangleF? cursorConfineRect; + + public RectangleF? CursorConfineRect + { + get => cursorConfineRect; + set + { + cursorConfineRect = value; + updateCursorConfinement(); + } + } + + private readonly Dictionary controllers = new Dictionary(); + + private void updateCursorVisibility(bool cursorVisible) => + ScheduleCommand(() => SDL_ShowCursor(cursorVisible ? SDL_ENABLE : SDL_DISABLE)); + + /// + /// Updates OS cursor confinement based on the current , and . + /// + private void updateCursorConfinement() + { + bool confined = CursorState.HasFlagFast(CursorState.Confined); + + ScheduleCommand(() => SDL_SetWindowGrab(SDLWindowHandle, confined ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); + + // Don't use SDL_SetWindowMouseRect when relative mode is enabled, as relative mode already confines the OS cursor to the window. + // This is fine for our use case, as UserInputManager will clamp the mouse position. + if (CursorConfineRect != null && confined && !RelativeMouseMode) + { + var rect = ((RectangleI)(CursorConfineRect / Scale)).ToSDLRect(); + ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, ref rect)); + } + else + { + ScheduleCommand(() => SDL_SetWindowMouseRect(SDLWindowHandle, IntPtr.Zero)); + } + } + + /// + /// Bound to . + /// + public readonly Bindable ConfineMouseMode = new Bindable(); + + private void enqueueJoystickAxisInput(JoystickAxisSource axisSource, short axisValue) + { + // SDL reports axis values in the range short.MinValue to short.MaxValue, so we scale and clamp it to the range of -1f to 1f + float clamped = Math.Clamp((float)axisValue / short.MaxValue, -1f, 1f); + JoystickAxisChanged?.Invoke(axisSource, clamped); + } + + private void enqueueJoystickButtonInput(JoystickButton button, bool isPressed) + { + if (isPressed) + JoystickButtonDown?.Invoke(button); + else + JoystickButtonUp?.Invoke(button); + } + + private Point previousPolledPoint = Point.Empty; + + private SDLButtonMask pressedButtons; + + private void pollMouse() + { + SDLButtonMask globalButtons = (SDLButtonMask)SDL_GetGlobalMouseState(out int x, out int y); + + if (previousPolledPoint.X != x || previousPolledPoint.Y != y) + { + previousPolledPoint = new Point(x, y); + + var pos = WindowMode.Value == Configuration.WindowMode.Windowed ? Position : windowDisplayBounds.Location; + int rx = x - pos.X; + int ry = y - pos.Y; + + MouseMove?.Invoke(new Vector2(rx * Scale, ry * Scale)); + } + + // a button should be released if it was pressed and its current global state differs (its bit in globalButtons is set to 0) + SDLButtonMask buttonsToRelease = pressedButtons & (globalButtons ^ pressedButtons); + + // the outer if just optimises for the common case that there are no buttons to release. + if (buttonsToRelease != SDLButtonMask.None) + { + if (buttonsToRelease.HasFlagFast(SDLButtonMask.Left)) MouseUp?.Invoke(MouseButton.Left); + if (buttonsToRelease.HasFlagFast(SDLButtonMask.Middle)) MouseUp?.Invoke(MouseButton.Middle); + if (buttonsToRelease.HasFlagFast(SDLButtonMask.Right)) MouseUp?.Invoke(MouseButton.Right); + if (buttonsToRelease.HasFlagFast(SDLButtonMask.X1)) MouseUp?.Invoke(MouseButton.Button1); + if (buttonsToRelease.HasFlagFast(SDLButtonMask.X2)) MouseUp?.Invoke(MouseButton.Button2); + } + } + + public virtual void StartTextInput(bool allowIme) => ScheduleCommand(SDL_StartTextInput); + + public void StopTextInput() => ScheduleCommand(SDL_StopTextInput); + + /// + /// Resets internal state of the platform-native IME. + /// This will clear its composition text and prepare it for new input. + /// + public virtual void ResetIme() => ScheduleCommand(() => + { + SDL_StopTextInput(); + SDL_StartTextInput(); + }); + + public void SetTextInputRect(RectangleF rect) => ScheduleCommand(() => + { + var sdlRect = ((RectangleI)(rect / Scale)).ToSDLRect(); + SDL_SetTextInputRect(ref sdlRect); + }); + + #region SDL Event Handling + + private void handleDropEvent(SDL_DropEvent evtDrop) + { + switch (evtDrop.type) + { + case SDL_EventType.SDL_DROPFILE: + string str = UTF8_ToManaged(evtDrop.file, true); + if (str != null) + DragDrop?.Invoke(str); + + break; + } + } + + private readonly long?[] activeTouches = new long?[TouchState.MAX_TOUCH_COUNT]; + + private TouchSource? getTouchSource(long fingerId) + { + for (int i = 0; i < activeTouches.Length; i++) + { + if (fingerId == activeTouches[i]) + return (TouchSource)i; + } + + return null; + } + + private TouchSource? assignNextAvailableTouchSource(long fingerId) + { + for (int i = 0; i < activeTouches.Length; i++) + { + if (activeTouches[i] != null) continue; + + activeTouches[i] = fingerId; + return (TouchSource)i; + } + + // we only handle up to TouchState.MAX_TOUCH_COUNT. Ignore any further touches for now. + return null; + } + + protected virtual void HandleTouchFingerEvent(SDL_TouchFingerEvent evtTfinger) + { + var existingSource = getTouchSource(evtTfinger.fingerId); + + if (evtTfinger.type == SDL_EventType.SDL_FINGERDOWN) + { + Debug.Assert(existingSource == null); + existingSource = assignNextAvailableTouchSource(evtTfinger.fingerId); + } + + if (existingSource == null) + return; + + float x = evtTfinger.x * ClientSize.Width; + float y = evtTfinger.y * ClientSize.Height; + + var touch = new Touch(existingSource.Value, new Vector2(x, y)); + + switch (evtTfinger.type) + { + case SDL_EventType.SDL_FINGERDOWN: + case SDL_EventType.SDL_FINGERMOTION: + TouchDown?.Invoke(touch); + break; + + case SDL_EventType.SDL_FINGERUP: + TouchUp?.Invoke(touch); + activeTouches[(int)existingSource] = null; + break; + } + } + + private void handleControllerDeviceEvent(SDL_ControllerDeviceEvent evtCdevice) + { + switch (evtCdevice.type) + { + case SDL_EventType.SDL_CONTROLLERDEVICEADDED: + addJoystick(evtCdevice.which); + break; + + case SDL_EventType.SDL_CONTROLLERDEVICEREMOVED: + SDL_GameControllerClose(controllers[evtCdevice.which].ControllerHandle); + controllers.Remove(evtCdevice.which); + break; + + case SDL_EventType.SDL_CONTROLLERDEVICEREMAPPED: + if (controllers.TryGetValue(evtCdevice.which, out var state)) + state.PopulateBindings(); + + break; + } + } + + private void handleControllerButtonEvent(SDL_ControllerButtonEvent evtCbutton) + { + var button = ((SDL_GameControllerButton)evtCbutton.button).ToJoystickButton(); + + switch (evtCbutton.type) + { + case SDL_EventType.SDL_CONTROLLERBUTTONDOWN: + enqueueJoystickButtonInput(button, true); + break; + + case SDL_EventType.SDL_CONTROLLERBUTTONUP: + enqueueJoystickButtonInput(button, false); + break; + } + } + + private void handleControllerAxisEvent(SDL_ControllerAxisEvent evtCaxis) => + enqueueJoystickAxisInput(((SDL_GameControllerAxis)evtCaxis.axis).ToJoystickAxisSource(), evtCaxis.axisValue); + + private void addJoystick(int which) + { + int instanceID = SDL_JoystickGetDeviceInstanceID(which); + + // if the joystick is already opened, ignore it + if (controllers.ContainsKey(instanceID)) + return; + + IntPtr joystick = SDL_JoystickOpen(which); + + IntPtr controller = IntPtr.Zero; + if (SDL_IsGameController(which) == SDL_bool.SDL_TRUE) + controller = SDL_GameControllerOpen(which); + + controllers[instanceID] = new SDL2ControllerBindings(joystick, controller); + } + + /// + /// Populates with joysticks that are already connected. + /// + private void populateJoysticks() + { + for (int i = 0; i < SDL_NumJoysticks(); i++) + { + addJoystick(i); + } + } + + private void handleJoyDeviceEvent(SDL_JoyDeviceEvent evtJdevice) + { + switch (evtJdevice.type) + { + case SDL_EventType.SDL_JOYDEVICEADDED: + addJoystick(evtJdevice.which); + 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); + break; + } + } + + private void handleJoyButtonEvent(SDL_JoyButtonEvent evtJbutton) + { + // if this button exists in the controller bindings, skip it + if (controllers.TryGetValue(evtJbutton.which, out var state) && state.IsJoystickButtonBound(evtJbutton.button)) + return; + + var button = JoystickButton.FirstButton + evtJbutton.button; + + switch (evtJbutton.type) + { + case SDL_EventType.SDL_JOYBUTTONDOWN: + enqueueJoystickButtonInput(button, true); + break; + + case SDL_EventType.SDL_JOYBUTTONUP: + enqueueJoystickButtonInput(button, false); + break; + } + } + + // ReSharper disable once UnusedParameter.Local + private void handleJoyHatEvent(SDL_JoyHatEvent evtJhat) + { + } + + // ReSharper disable once UnusedParameter.Local + private void handleJoyBallEvent(SDL_JoyBallEvent evtJball) + { + } + + private void handleJoyAxisEvent(SDL_JoyAxisEvent evtJaxis) + { + // if this axis exists in the controller bindings, skip it + if (controllers.TryGetValue(evtJaxis.which, out var state) && state.IsJoystickAxisBound(evtJaxis.axis)) + return; + + enqueueJoystickAxisInput(JoystickAxisSource.Axis1 + evtJaxis.axis, evtJaxis.axisValue); + } + + private uint lastPreciseScroll; + private const uint precise_scroll_debounce = 100; + + private void handleMouseWheelEvent(SDL_MouseWheelEvent evtWheel) + { + bool isPrecise(float f) => f % 1 != 0; + + if (isPrecise(evtWheel.preciseX) || isPrecise(evtWheel.preciseY)) + lastPreciseScroll = evtWheel.timestamp; + + bool precise = evtWheel.timestamp < lastPreciseScroll + precise_scroll_debounce; + + // SDL reports horizontal scroll opposite of what framework expects (in non-"natural" mode, scrolling to the right gives positive deltas while we want negative). + TriggerMouseWheel(new Vector2(-evtWheel.preciseX, evtWheel.preciseY), precise); + } + + private void handleMouseButtonEvent(SDL_MouseButtonEvent evtButton) + { + MouseButton button = mouseButtonFromEvent(evtButton.button); + SDLButtonMask mask = (SDLButtonMask)SDL_BUTTON(evtButton.button); + Debug.Assert(Enum.IsDefined(mask)); + + switch (evtButton.type) + { + case SDL_EventType.SDL_MOUSEBUTTONDOWN: + pressedButtons |= mask; + MouseDown?.Invoke(button); + break; + + case SDL_EventType.SDL_MOUSEBUTTONUP: + pressedButtons &= ~mask; + MouseUp?.Invoke(button); + break; + } + } + + private void handleMouseMotionEvent(SDL_MouseMotionEvent evtMotion) + { + if (SDL_GetRelativeMouseMode() == SDL_bool.SDL_FALSE) + MouseMove?.Invoke(new Vector2(evtMotion.x * Scale, evtMotion.y * Scale)); + else + MouseMoveRelative?.Invoke(new Vector2(evtMotion.xrel * Scale, evtMotion.yrel * Scale)); + } + + protected virtual unsafe void HandleTextInputEvent(SDL_TextInputEvent evtText) + { + if (!SDL2Extensions.TryGetStringFromBytePointer(evtText.text, out string text)) + return; + + TriggerTextInput(text); + } + + protected virtual unsafe void HandleTextEditingEvent(SDL_TextEditingEvent evtEdit) + { + if (!SDL2Extensions.TryGetStringFromBytePointer(evtEdit.text, out string text)) + return; + + TriggerTextEditing(text, evtEdit.start, evtEdit.length); + } + + private void handleKeyboardEvent(SDL_KeyboardEvent evtKey) + { + Key key = evtKey.keysym.ToKey(); + + if (key == Key.Unknown) + { + Logger.Log($"Unknown SDL key: {evtKey.keysym.scancode}, {evtKey.keysym.sym}"); + return; + } + + switch (evtKey.type) + { + case SDL_EventType.SDL_KEYDOWN: + KeyDown?.Invoke(key); + break; + + case SDL_EventType.SDL_KEYUP: + KeyUp?.Invoke(key); + break; + } + } + + private void handleKeymapChangedEvent() => KeymapChanged?.Invoke(); + + private MouseButton mouseButtonFromEvent(byte button) + { + switch ((uint)button) + { + default: + case SDL_BUTTON_LEFT: + return MouseButton.Left; + + case SDL_BUTTON_RIGHT: + return MouseButton.Right; + + case SDL_BUTTON_MIDDLE: + return MouseButton.Middle; + + case SDL_BUTTON_X1: + return MouseButton.Button1; + + case SDL_BUTTON_X2: + return MouseButton.Button2; + } + } + + /// + /// Button mask as returned from and . + /// + [Flags] + private enum SDLButtonMask + { + None = 0, + + /// + Left = 1 << 0, + + /// + Middle = 1 << 1, + + /// + Right = 1 << 2, + + /// + X1 = 1 << 3, + + /// + X2 = 1 << 4 + } + + #endregion + + /// + /// Update the host window manager's cursor position based on a location relative to window coordinates. + /// + /// A position inside the window. + public void UpdateMousePosition(Vector2 mousePosition) => ScheduleCommand(() => + SDL_WarpMouseInWindow(SDLWindowHandle, (int)(mousePosition.X / Scale), (int)(mousePosition.Y / Scale))); + + private void updateConfineMode() + { + bool confine = false; + + switch (ConfineMouseMode.Value) + { + case Input.ConfineMouseMode.Fullscreen: + confine = WindowMode.Value != Configuration.WindowMode.Windowed; + break; + + case Input.ConfineMouseMode.Always: + confine = true; + break; + } + + if (confine) + CursorStateBindable.Value |= CursorState.Confined; + else + CursorStateBindable.Value &= ~CursorState.Confined; + } + + #region Events + + /// + /// Invoked when the mouse cursor enters the window. + /// + public event Action? MouseEntered; + + /// + /// Invoked when the mouse cursor leaves the window. + /// + public event Action? MouseLeft; + + /// + /// Invoked when the user scrolls the mouse wheel over the window. + /// + /// + /// Delta is positive when mouse wheel scrolled to the up or left, in non-"natural" scroll mode (ie. the classic way). + /// + public event Action? MouseWheel; + + protected void TriggerMouseWheel(Vector2 delta, bool precise) => MouseWheel?.Invoke(delta, precise); + + /// + /// Invoked when the user moves the mouse cursor within the window. + /// + public event Action? MouseMove; + + protected void TriggerMouseMove(float x, float y) => MouseMove?.Invoke(new Vector2(x, y)); + + /// + /// Invoked when the user moves the mouse cursor within the window (via relative / raw input). + /// + public event Action? MouseMoveRelative; + + /// + /// Invoked when the user presses a mouse button. + /// + public event Action? MouseDown; + + protected void TriggerMouseDown(MouseButton button) => MouseDown?.Invoke(button); + + /// + /// Invoked when the user releases a mouse button. + /// + public event Action? MouseUp; + + protected void TriggerMouseUp(MouseButton button) => MouseUp?.Invoke(button); + + /// + /// Invoked when the user presses a key. + /// + public event Action? KeyDown; + + /// + /// Invoked when the user releases a key. + /// + public event Action? KeyUp; + + /// + /// Invoked when the user enters text. + /// + public event Action? TextInput; + + protected void TriggerTextInput(string text) => TextInput?.Invoke(text); + + /// + /// Invoked when an IME text editing event occurs. + /// + public event TextEditingDelegate? TextEditing; + + protected void TriggerTextEditing(string text, int start, int length) => TextEditing?.Invoke(text, start, length); + + /// + public event Action? KeymapChanged; + + /// + /// Invoked when a joystick axis changes. + /// + public event Action? JoystickAxisChanged; + + /// + /// Invoked when the user presses a button on a joystick. + /// + public event Action? JoystickButtonDown; + + /// + /// Invoked when the user releases a button on a joystick. + /// + public event Action? JoystickButtonUp; + + /// + /// Invoked when a finger moves or touches a touchscreen. + /// + public event Action? TouchDown; + + /// + /// Invoked when a finger leaves the touchscreen. + /// + public event Action? TouchUp; + + #endregion + + /// + /// Fired when text is edited, usually via IME composition. + /// + /// The composition text. + /// The index of the selection start. + /// The length of the selection. + public delegate void TextEditingDelegate(string text, int start, int length); + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs b/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs new file mode 100644 index 0000000000..bdac81f47a --- /dev/null +++ b/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs @@ -0,0 +1,889 @@ +// 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.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Configuration; +using osu.Framework.Logging; +using osuTK; +using static SDL2.SDL; + +namespace osu.Framework.Platform.SDL2 +{ + internal partial class SDL2Window + { + private void setupWindowing(FrameworkConfigManager config) + { + config.BindWith(FrameworkSetting.MinimiseOnFocusLossInFullscreen, minimiseOnFocusLoss); + minimiseOnFocusLoss.BindValueChanged(e => + { + ScheduleCommand(() => SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, e.NewValue ? "1" : "0")); + }, true); + + fetchDisplays(); + + DisplaysChanged += _ => CurrentDisplayBindable.Default = PrimaryDisplay; + CurrentDisplayBindable.Default = PrimaryDisplay; + CurrentDisplayBindable.ValueChanged += evt => + { + windowDisplayIndexBindable.Value = (DisplayIndex)evt.NewValue.Index; + }; + + config.BindWith(FrameworkSetting.LastDisplayDevice, windowDisplayIndexBindable); + windowDisplayIndexBindable.BindValueChanged(evt => + { + currentDisplay = Displays.ElementAtOrDefault((int)evt.NewValue) ?? PrimaryDisplay; + invalidateWindowSpecifics(); + }, true); + + sizeFullscreen.ValueChanged += _ => + { + if (storingSizeToConfig) return; + if (windowState != WindowState.Fullscreen) return; + + invalidateWindowSpecifics(); + }; + + sizeWindowed.ValueChanged += _ => + { + if (storingSizeToConfig) return; + if (windowState != WindowState.Normal) return; + + invalidateWindowSpecifics(); + }; + + config.BindWith(FrameworkSetting.WindowedSize, sizeWindowed); + + sizeWindowed.MinValueChanged += min => + { + if (min.Width < 0 || min.Height < 0) + throw new InvalidOperationException($"Expected zero or positive size, got {min}"); + + if (min.Width > sizeWindowed.MaxValue.Width || min.Height > sizeWindowed.MaxValue.Height) + throw new InvalidOperationException($"Expected a size less than max window size ({sizeWindowed.MaxValue}), got {min}"); + + ScheduleCommand(() => SDL_SetWindowMinimumSize(SDLWindowHandle, min.Width, min.Height)); + }; + + sizeWindowed.MaxValueChanged += max => + { + if (max.Width <= 0 || max.Height <= 0) + throw new InvalidOperationException($"Expected positive size, got {max}"); + + if (max.Width < sizeWindowed.MinValue.Width || max.Height < sizeWindowed.MinValue.Height) + throw new InvalidOperationException($"Expected a size greater than min window size ({sizeWindowed.MinValue}), got {max}"); + + ScheduleCommand(() => SDL_SetWindowMaximumSize(SDLWindowHandle, max.Width, max.Height)); + }; + + config.BindWith(FrameworkSetting.SizeFullscreen, sizeFullscreen); + + config.BindWith(FrameworkSetting.WindowedPositionX, windowPositionX); + config.BindWith(FrameworkSetting.WindowedPositionY, windowPositionY); + + config.BindWith(FrameworkSetting.WindowMode, WindowMode); + + WindowMode.BindValueChanged(evt => + { + switch (evt.NewValue) + { + case Configuration.WindowMode.Fullscreen: + WindowState = WindowState.Fullscreen; + break; + + case Configuration.WindowMode.Borderless: + WindowState = WindowState.FullscreenBorderless; + break; + + case Configuration.WindowMode.Windowed: + WindowState = windowMaximised ? WindowState.Maximised : WindowState.Normal; + break; + } + }); + } + + private void initialiseWindowingAfterCreation() + { + updateAndFetchWindowSpecifics(); + fetchWindowSize(); + + sizeWindowed.TriggerChange(); + + WindowMode.TriggerChange(); + } + + private bool focused; + + /// + /// Whether the window currently has focus. + /// + public bool Focused + { + get => focused; + private set + { + if (value == focused) + return; + + isActive.Value = focused = value; + } + } + + public WindowMode DefaultWindowMode => RuntimeInfo.IsMobile ? Configuration.WindowMode.Fullscreen : Configuration.WindowMode.Windowed; + + /// + public virtual IEnumerable SupportedWindowModes + { + get + { + if (RuntimeInfo.IsMobile) + return new[] { Configuration.WindowMode.Fullscreen }; + + return Enum.GetValues(); + } + } + + private Point position; + + /// + /// Returns or sets the window's position in screen space. Only valid when in + /// + public Point Position + { + get => position; + set + { + position = value; + ScheduleCommand(() => SDL_SetWindowPosition(SDLWindowHandle, value.X, value.Y)); + } + } + + private bool resizable = true; + + /// + /// Returns or sets whether the window is resizable or not. Only valid when in . + /// + public bool Resizable + { + get => resizable; + set + { + if (resizable == value) + return; + + resizable = value; + ScheduleCommand(() => SDL_SetWindowResizable(SDLWindowHandle, value ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE)); + } + } + + private Size size = new Size(default_width, default_height); + + /// + /// Returns or sets the window's internal size, before scaling. + /// + public virtual Size Size + { + get => size; + protected set + { + if (value.Equals(size)) return; + + size = value; + Resized?.Invoke(); + } + } + + public Size MinSize + { + get => sizeWindowed.MinValue; + set => sizeWindowed.MinValue = value; + } + + public Size MaxSize + { + get => sizeWindowed.MaxValue; + set => sizeWindowed.MaxValue = value; + } + + public Bindable CurrentDisplayBindable { get; } = new Bindable(); + + /// + /// Bound to . + /// + public Bindable WindowMode { get; } = new Bindable(); + + private readonly BindableBool isActive = new BindableBool(); + + public IBindable IsActive => isActive; + + private readonly BindableBool cursorInWindow = new BindableBool(); + + public IBindable CursorInWindow => cursorInWindow; + + private bool visible; + + /// + /// Enables or disables the window visibility. + /// + public bool Visible + { + get => visible; + set + { + visible = value; + ScheduleCommand(() => + { + if (value) + SDL_ShowWindow(SDLWindowHandle); + else + SDL_HideWindow(SDLWindowHandle); + }); + } + } + + private WindowState windowState = WindowState.Normal; + + private WindowState? pendingWindowState; + + /// + /// Returns or sets the window's current . + /// + public WindowState WindowState + { + get => windowState; + set + { + if (pendingWindowState == null && windowState == value) + return; + + pendingWindowState = value; + } + } + + /// + /// Stores whether the window used to be in maximised state or not. + /// Used to properly decide what window state to pick when switching to windowed mode (see change event) + /// + private bool windowMaximised; + + /// + /// Returns the drawable area, after scaling. + /// + public Size ClientSize => new Size((int)(Size.Width * Scale), (int)(Size.Height * Scale)); + + public float Scale = 1; + + #region Displays (mostly self-contained) + + /// + /// Queries the physical displays and their supported resolutions. + /// + public ImmutableArray Displays { get; private set; } = ImmutableArray.Empty; + + public event Action>? DisplaysChanged; + + // ReSharper disable once UnusedParameter.Local + private void handleDisplayEvent(SDL_DisplayEvent evtDisplay) => fetchDisplays(); + + /// + /// Updates with the latest display information reported by SDL. + /// + /// + /// Has no effect on values of + /// / + /// . + /// + private void fetchDisplays() + { + Displays = getSDLDisplays(); + DisplaysChanged?.Invoke(Displays); + } + + /// + /// Asserts that the current match the actual displays as reported by + /// + /// + /// This assert is not fatal, as the will get updated sooner or later + /// in or . + /// + [Conditional("DEBUG")] + private void assertDisplaysMatchSDL() + { + var actualDisplays = getSDLDisplays(); + + const string message = $"Stored {nameof(Displays)} don't match actual displays"; + string detailedMessage = $"Stored displays:\n {string.Join("\n ", Displays)}\n\nActual displays:\n {string.Join("\n ", actualDisplays)}"; + + Debug.Assert(actualDisplays.SequenceEqual(Displays), message, detailedMessage); + } + + private static ImmutableArray getSDLDisplays() + { + int numDisplays = SDL_GetNumVideoDisplays(); + + if (numDisplays <= 0) + throw new InvalidOperationException($"Failed to get number of SDL displays. Return code: {numDisplays}. SDL Error: {SDL_GetError()}"); + + var builder = ImmutableArray.CreateBuilder(numDisplays); + + for (int i = 0; i < numDisplays; i++) + { + if (tryGetDisplayFromSDL(i, out Display? display)) + builder.Add(display); + else + Logger.Log($"Failed to retrieve SDL display at index ({i})", level: LogLevel.Error); + } + + return builder.MoveToImmutable(); + } + + private static bool tryGetDisplayFromSDL(int displayIndex, [NotNullWhen(true)] out Display? display) + { + ArgumentOutOfRangeException.ThrowIfNegative(displayIndex); + + if (SDL_GetDisplayBounds(displayIndex, out var rect) < 0) + { + Logger.Log($"Failed to get display bounds for display at index ({displayIndex}). SDL Error: {SDL_GetError()}"); + display = null; + return false; + } + + DisplayMode[] displayModes = Array.Empty(); + + if (RuntimeInfo.IsDesktop) + { + int numModes = SDL_GetNumDisplayModes(displayIndex); + + if (numModes < 0) + { + Logger.Log($"Failed to get display modes for display at index ({displayIndex}) ({rect.w}x{rect.h}). SDL Error: {SDL_GetError()} ({numModes})"); + display = null; + return false; + } + + if (numModes == 0) + Logger.Log($"Display at index ({displayIndex}) ({rect.w}x{rect.h}) has no display modes. Fullscreen might not work."); + + displayModes = Enumerable.Range(0, numModes) + .Select(modeIndex => + { + SDL_GetDisplayMode(displayIndex, modeIndex, out var mode); + return mode.ToDisplayMode(displayIndex); + }) + .ToArray(); + } + + display = new Display(displayIndex, SDL_GetDisplayName(displayIndex), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes); + return true; + } + + #endregion + + /// + /// Gets the that has been set as "primary" or "default" in the operating system. + /// + public virtual Display PrimaryDisplay => Displays.First(); + + private Display currentDisplay = null!; + private int displayIndex = -1; + + private readonly Bindable currentDisplayMode = new Bindable(); + + /// + /// The for the display that this window is currently on. + /// + public IBindable CurrentDisplayMode => currentDisplayMode; + + private Rectangle windowDisplayBounds + { + get + { + SDL_GetDisplayBounds(displayIndex, out var rect); + return new Rectangle(rect.x, rect.y, rect.w, rect.h); + } + } + + /// + /// Bound to . + /// + private readonly BindableSize sizeFullscreen = new BindableSize(); + + /// + /// Bound to . + /// + private readonly BindableSize sizeWindowed = new BindableSize(); + + /// + /// Bound to . + /// + private readonly BindableDouble windowPositionX = new BindableDouble(); + + /// + /// Bound to . + /// + private readonly BindableDouble windowPositionY = new BindableDouble(); + + /// + /// Bound to . + /// + private readonly Bindable windowDisplayIndexBindable = new Bindable(); + + private readonly BindableBool minimiseOnFocusLoss = new BindableBool(); + + /// + /// Updates and according to SDL state. + /// + /// Whether the window size has been changed after updating. + private void fetchWindowSize() + { + SDL_GetWindowSize(SDLWindowHandle, out int w, out int h); + + int drawableW = graphicsSurface.GetDrawableSize().Width; + + // When minimised on windows, values may be zero. + // If we receive zeroes for either of these, it seems safe to completely ignore them. + if (w <= 0 || drawableW <= 0) + return; + + Scale = (float)drawableW / w; + Size = new Size(w, h); + + storeWindowSizeToConfig(); + } + + #region SDL Event Handling + + private void handleWindowEvent(SDL_WindowEvent evtWindow) + { + updateAndFetchWindowSpecifics(); + + switch (evtWindow.windowEvent) + { + case SDL_WindowEventID.SDL_WINDOWEVENT_MOVED: + // explicitly requery as there are occasions where what SDL has provided us with is not up-to-date. + SDL_GetWindowPosition(SDLWindowHandle, out int x, out int y); + var newPosition = new Point(x, y); + + if (!newPosition.Equals(Position)) + { + position = newPosition; + Moved?.Invoke(newPosition); + + if (WindowMode.Value == Configuration.WindowMode.Windowed) + storeWindowPositionToConfig(); + } + + break; + + case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED: + fetchWindowSize(); + break; + + case SDL_WindowEventID.SDL_WINDOWEVENT_ENTER: + cursorInWindow.Value = true; + MouseEntered?.Invoke(); + break; + + case SDL_WindowEventID.SDL_WINDOWEVENT_LEAVE: + cursorInWindow.Value = false; + MouseLeft?.Invoke(); + break; + + case SDL_WindowEventID.SDL_WINDOWEVENT_RESTORED: + case SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED: + Focused = true; + break; + + case SDL_WindowEventID.SDL_WINDOWEVENT_MINIMIZED: + case SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST: + Focused = false; + break; + + case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE: + break; + } + + // displays can change without a SDL_DISPLAYEVENT being sent, eg. changing resolution. + // force update displays when gaining keyboard focus to always have up-to-date information. + // eg. this covers scenarios when changing resolution outside of the game, and then tabbing in. + switch (evtWindow.windowEvent) + { + case SDL_WindowEventID.SDL_WINDOWEVENT_RESTORED: + case SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_GAINED: + case SDL_WindowEventID.SDL_WINDOWEVENT_MINIMIZED: + case SDL_WindowEventID.SDL_WINDOWEVENT_FOCUS_LOST: + case SDL_WindowEventID.SDL_WINDOWEVENT_SHOWN: + case SDL_WindowEventID.SDL_WINDOWEVENT_HIDDEN: + fetchDisplays(); + break; + } + +#if DEBUG + EventScheduler.AddOnce(() => assertDisplaysMatchSDL()); +#endif + } + + /// + /// Invalidates the the state of the window. + /// This forces to run before the next event loop. + /// + private void invalidateWindowSpecifics() + { + pendingWindowState = windowState; + } + + /// + /// Should be run on a regular basis to check for external window state changes. + /// + private void updateAndFetchWindowSpecifics() + { + // don't attempt to run before the window is initialised, as Create() will do so anyway. + if (SDLWindowHandle == IntPtr.Zero) + return; + + var stateBefore = windowState; + + // check for a pending user state change and give precedence. + if (pendingWindowState != null) + { + windowState = pendingWindowState.Value; + pendingWindowState = null; + + updatingWindowStateAndSize = true; + UpdateWindowStateAndSize(windowState, currentDisplay, currentDisplayMode.Value); + updatingWindowStateAndSize = false; + + fetchWindowSize(); + + if (tryFetchMaximisedState(windowState, out bool maximized)) + windowMaximised = maximized; + + if (tryFetchDisplayMode(SDLWindowHandle, windowState, currentDisplay, out var newMode)) + currentDisplayMode.Value = newMode; + + fetchDisplays(); + } + else + { + windowState = ((SDL_WindowFlags)SDL_GetWindowFlags(SDLWindowHandle)).ToWindowState(); + } + + if (windowState != stateBefore) + { + WindowStateChanged?.Invoke(windowState); + + if (tryFetchMaximisedState(windowState, out bool maximized)) + windowMaximised = maximized; + } + + int newDisplayIndex = SDL_GetWindowDisplayIndex(SDLWindowHandle); + + if (displayIndex != newDisplayIndex) + { + displayIndex = newDisplayIndex; + currentDisplay = Displays.ElementAtOrDefault(displayIndex) ?? PrimaryDisplay; + CurrentDisplayBindable.Value = currentDisplay; + } + } + + /// + /// Should be run after a local window state change, to propagate the correct SDL actions. + /// + /// + /// Call sites need to set appropriately. + /// + protected virtual void UpdateWindowStateAndSize(WindowState state, Display display, DisplayMode displayMode) + { + switch (state) + { + case WindowState.Normal: + Size = sizeWindowed.Value; + + SDL_RestoreWindow(SDLWindowHandle); + SDL_SetWindowSize(SDLWindowHandle, Size.Width, Size.Height); + SDL_SetWindowResizable(SDLWindowHandle, Resizable ? SDL_bool.SDL_TRUE : SDL_bool.SDL_FALSE); + + readWindowPositionFromConfig(state, display); + break; + + case WindowState.Fullscreen: + var closestMode = getClosestDisplayMode(SDLWindowHandle, sizeFullscreen.Value, display, displayMode); + + Size = new Size(closestMode.w, closestMode.h); + + ensureWindowOnDisplay(display); + + SDL_SetWindowDisplayMode(SDLWindowHandle, ref closestMode); + SDL_SetWindowFullscreen(SDLWindowHandle, (uint)SDL_WindowFlags.SDL_WINDOW_FULLSCREEN); + break; + + case WindowState.FullscreenBorderless: + Size = SetBorderless(display); + break; + + case WindowState.Maximised: + SDL_RestoreWindow(SDLWindowHandle); + + ensureWindowOnDisplay(display); + + SDL_MaximizeWindow(SDLWindowHandle); + break; + + case WindowState.Minimised: + ensureWindowOnDisplay(display); + SDL_MinimizeWindow(SDLWindowHandle); + break; + } + } + + private static bool tryFetchDisplayMode(IntPtr windowHandle, WindowState windowState, Display display, out DisplayMode displayMode) + { + // TODO: displayIndex should be valid here at all times. + // on startup, the displayIndex will be invalid (-1) due to it being set later in the startup sequence. + // related to order of operations in `updateWindowSpecifics()`. + int localIndex = SDL_GetWindowDisplayIndex(windowHandle); + + if (localIndex != display.Index) + Logger.Log($"Stored display index ({display.Index}) doesn't match current index ({localIndex})"); + + bool success; + SDL_DisplayMode mode; + + if (windowState == WindowState.Fullscreen) + success = SDL_GetWindowDisplayMode(windowHandle, out mode) >= 0; + else + success = SDL_GetCurrentDisplayMode(localIndex, out mode) >= 0; + + string type = windowState == WindowState.Fullscreen ? "fullscreen" : "desktop"; + + if (success) + { + displayMode = mode.ToDisplayMode(localIndex); + Logger.Log($"Updated display mode to {type} resolution: {mode.w}x{mode.h}@{mode.refresh_rate}, {displayMode.Format}"); + return true; + } + else + { + Logger.Log($"Failed to get {type} display mode. Display index: {localIndex}. SDL error: {SDL_GetError()}"); + displayMode = default; + return false; + } + } + + private static bool tryFetchMaximisedState(WindowState windowState, out bool maximized) + { + if (windowState is WindowState.Normal or WindowState.Maximised) + { + maximized = windowState == WindowState.Maximised; + return true; + } + + maximized = default; + return false; + } + + private void readWindowPositionFromConfig(WindowState state, Display display) + { + if (state != WindowState.Normal) + return; + + var configPosition = new Vector2((float)windowPositionX.Value, (float)windowPositionY.Value); + + moveWindowTo(display, configPosition); + } + + /// + /// Ensures that the window is located on the provided . + /// + /// The to center the window on. + private void ensureWindowOnDisplay(Display display) + { + if (display.Index == SDL_GetWindowDisplayIndex(SDLWindowHandle)) + return; + + moveWindowTo(display, new Vector2(0.5f)); + } + + /// + /// Moves the window to be centred around the normalised on a . + /// + /// The to move the window to. + /// Relative position on the display, normalised to [-0.5, 1.5]. + private void moveWindowTo(Display display, Vector2 newPosition) + { + Debug.Assert(newPosition == Vector2.Clamp(newPosition, new Vector2(-0.5f), new Vector2(1.5f))); + + var displayBounds = display.Bounds; + var windowSize = sizeWindowed.Value; + int windowX = (int)Math.Round((displayBounds.Width - windowSize.Width) * newPosition.X); + int windowY = (int)Math.Round((displayBounds.Height - windowSize.Height) * newPosition.Y); + + Position = new Point(windowX + displayBounds.X, windowY + displayBounds.Y); + } + + private void storeWindowPositionToConfig() + { + if (WindowState != WindowState.Normal) + return; + + var displayBounds = currentDisplay.Bounds; + + int windowX = Position.X - displayBounds.X; + int windowY = Position.Y - displayBounds.Y; + + var windowSize = sizeWindowed.Value; + + windowPositionX.Value = displayBounds.Width > windowSize.Width ? (float)windowX / (displayBounds.Width - windowSize.Width) : 0; + windowPositionY.Value = displayBounds.Height > windowSize.Height ? (float)windowY / (displayBounds.Height - windowSize.Height) : 0; + } + + /// + /// Set to true while the window size is being stored to config to avoid bindable feedback. + /// + private bool storingSizeToConfig; + + /// + /// Set when is in progress to avoid being called with invalid data. + /// + /// + /// Since is a multi-step process, intermediary windows size changes might be invalid. + /// This is usually not a problem, but since runs out-of-band, invalid data might appear in those events. + /// + private bool updatingWindowStateAndSize; + + private void storeWindowSizeToConfig() + { + if (WindowState != WindowState.Normal) + return; + + storingSizeToConfig = true; + sizeWindowed.Value = Size; + storingSizeToConfig = false; + } + + /// + /// Prepare display of a borderless window. + /// + /// The display to make the window fullscreen borderless on. + /// + /// The size of the borderless window's draw area. + /// + protected virtual Size SetBorderless(Display display) + { + ensureWindowOnDisplay(display); + + // this is a generally sane method of handling borderless, and works well on macOS and linux. + SDL_SetWindowFullscreen(SDLWindowHandle, (uint)SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP); + + return display.Bounds.Size; + } + + #endregion + + public void CycleMode() + { + var currentValue = WindowMode.Value; + + do + { + switch (currentValue) + { + case Configuration.WindowMode.Windowed: + currentValue = Configuration.WindowMode.Borderless; + break; + + case Configuration.WindowMode.Borderless: + currentValue = Configuration.WindowMode.Fullscreen; + break; + + case Configuration.WindowMode.Fullscreen: + currentValue = Configuration.WindowMode.Windowed; + break; + } + } while (!SupportedWindowModes.Contains(currentValue) && currentValue != WindowMode.Value); + + WindowMode.Value = currentValue; + } + + #region Helper functions + + private static SDL_DisplayMode getClosestDisplayMode(IntPtr windowHandle, Size size, Display display, DisplayMode requestedMode) + { + SDL_ClearError(); // clear any stale error. + + // default size means to use the display's native size. + if (size.Width == 9999 && size.Height == 9999) + size = display.Bounds.Size; + + var targetMode = new SDL_DisplayMode { w = size.Width, h = size.Height, refresh_rate = (int)Math.Round(requestedMode.RefreshRate) }; + + if (SDL_GetClosestDisplayMode(display.Index, ref targetMode, out var mode) != IntPtr.Zero) + return mode; + else + Logger.Log($"Unable to get preferred display mode (try #1/2). Target display: {display.Index}, mode: {targetMode.ReadableString()}. SDL error: {SDL2Extensions.GetAndClearError()}"); + + // fallback to current display's native bounds + targetMode.w = display.Bounds.Width; + targetMode.h = display.Bounds.Height; + targetMode.refresh_rate = 0; + + if (SDL_GetClosestDisplayMode(display.Index, ref targetMode, out mode) != IntPtr.Zero) + return mode; + else + Logger.Log($"Unable to get preferred display mode (try #2/2). Target display: {display.Index}, mode: {targetMode.ReadableString()}. SDL error: {SDL2Extensions.GetAndClearError()}"); + + // try the display's native display mode. + if (SDL_GetDesktopDisplayMode(display.Index, out mode) == 0) + return mode; + else + Logger.Log($"Failed to get desktop display mode (try #1/3). Target display: {display.Index}. SDL error: {SDL2Extensions.GetAndClearError()}", level: LogLevel.Error); + + // try the primary display mode. + if (SDL_GetDisplayMode(display.Index, 0, out mode) == 0) + return mode; + else + Logger.Log($"Failed to get desktop display mode (try #2/3). Target display: {display.Index}. SDL error: {SDL2Extensions.GetAndClearError()}", level: LogLevel.Error); + + // try the primary display's primary display mode. + if (SDL_GetDisplayMode(0, 0, out mode) == 0) + return mode; + else + Logger.Log($"Failed to get desktop display mode (try #3/3). Target display: primary. SDL error: {SDL2Extensions.GetAndClearError()}", level: LogLevel.Error); + + // finally return the current mode if everything else fails. + if (SDL_GetWindowDisplayMode(windowHandle, out mode) >= 0) + return mode; + else + Logger.Log($"Failed to get window display mode. SDL error: {SDL2Extensions.GetAndClearError()}", level: LogLevel.Error); + + throw new InvalidOperationException("couldn't retrieve valid display mode"); + } + + #endregion + + #region Events + + /// + /// Invoked after the window has resized. + /// + public event Action? Resized; + + /// + /// Invoked after the window's state has changed. + /// + public event Action? WindowStateChanged; + + /// + /// Invoked when the window moves. + /// + public event Action? Moved; + + #endregion + } +} diff --git a/osu.Framework/Platform/SDL2GameHost.cs b/osu.Framework/Platform/SDL2GameHost.cs new file mode 100644 index 0000000000..b59e2be00a --- /dev/null +++ b/osu.Framework/Platform/SDL2GameHost.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Input; +using osu.Framework.Input.Handlers; +using osu.Framework.Input.Handlers.Joystick; +using osu.Framework.Input.Handlers.Keyboard; +using osu.Framework.Input.Handlers.Midi; +using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Input.Handlers.Tablet; +using osu.Framework.Input.Handlers.Touch; +using osu.Framework.Platform.SDL2; + +namespace osu.Framework.Platform +{ + public abstract class SDL2GameHost : GameHost + { + public override bool CapsLockEnabled => (Window as SDL2Window)?.CapsLockPressed == true; + + protected SDL2GameHost(string gameName, HostOptions? options = null) + : base(gameName, options) + { + } + + protected override TextInputSource CreateTextInput() + { + if (Window is SDL2Window window) + return new SDL2WindowTextInput(window); + + return base.CreateTextInput(); + } + + protected override Clipboard CreateClipboard() => new SDL2Clipboard(); + + protected override IEnumerable CreateAvailableInputHandlers() => + new InputHandler[] + { + new KeyboardHandler(), + // tablet should get priority over mouse to correctly handle cases where tablet drivers report as mice as well. + new OpenTabletDriverHandler(), + new MouseHandler(), + new TouchHandler(), + new JoystickHandler(), + new MidiHandler(), + }; + } +} From 8d7832afb98ac7df915f1dab8867b79648912123 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 17:00:06 +0900 Subject: [PATCH 041/140] Add app name support --- osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs | 4 ++-- osu.Framework/Platform/SDL2/SDL2Window.cs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs b/osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs index 94c5795417..2ead150f3c 100644 --- a/osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs +++ b/osu.Framework/Platform/SDL2/SDL2DesktopWindow.cs @@ -7,8 +7,8 @@ namespace osu.Framework.Platform.SDL2 { internal class SDL2DesktopWindow : SDL2Window { - public SDL2DesktopWindow(GraphicsSurfaceType surfaceType) - : base(surfaceType) + public SDL2DesktopWindow(GraphicsSurfaceType surfaceType, string appName) + : base(surfaceType, appName) { } diff --git a/osu.Framework/Platform/SDL2/SDL2Window.cs b/osu.Framework/Platform/SDL2/SDL2Window.cs index 3b6564993c..7feff6327f 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window.cs @@ -181,10 +181,12 @@ internal SDL_SysWMinfo GetWindowSystemInformation() /// protected ObjectHandle ObjectHandle { get; private set; } - protected SDL2Window(GraphicsSurfaceType surfaceType) + protected SDL2Window(GraphicsSurfaceType surfaceType, string appName) { ObjectHandle = new ObjectHandle(this, GCHandleType.Normal); + SDL_SetHint(SDL_HINT_APP_NAME, appName); + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { throw new InvalidOperationException($"Failed to initialise SDL: {SDL_GetError()}"); From 21dedbbef2f1af76eec23048f9ac95d389f909ca Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 17:36:14 +0900 Subject: [PATCH 042/140] Add ISDLWindow abstraction --- .../Handlers/Joystick/JoystickHandler.cs | 3 +- .../Handlers/Keyboard/KeyboardHandler.cs | 3 +- .../Input/Handlers/Mouse/MouseHandler.cs | 7 ++-- .../Input/Handlers/Touch/TouchHandler.cs | 3 +- osu.Framework/Input/UserInputManager.cs | 3 +- osu.Framework/Platform/ISDLWindow.cs | 37 +++++++++++++++++++ osu.Framework/Platform/SDL2/SDL2Window.cs | 2 +- .../Platform/SDL2/SDL2Window_Input.cs | 2 +- osu.Framework/Platform/SDL3/SDL3Window.cs | 2 +- .../Platform/SDL3/SDL3Window_Input.cs | 2 +- 10 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 osu.Framework/Platform/ISDLWindow.cs diff --git a/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs b/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs index 55f92ce66e..2547ec475f 100644 --- a/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs +++ b/osu.Framework/Input/Handlers/Joystick/JoystickHandler.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Input.StateChanges; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; namespace osu.Framework.Input.Handlers.Joystick @@ -30,7 +29,7 @@ public override bool Initialize(GameHost host) if (!base.Initialize(host)) return false; - if (!(host.Window is SDL3Window window)) + if (!(host.Window is ISDLWindow window)) return false; Enabled.BindValueChanged(e => diff --git a/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs b/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs index 31bb8b7836..7b8afb4da2 100644 --- a/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs +++ b/osu.Framework/Input/Handlers/Keyboard/KeyboardHandler.cs @@ -3,7 +3,6 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; using TKKey = osuTK.Input.Key; @@ -22,7 +21,7 @@ public override bool Initialize(GameHost host) if (!base.Initialize(host)) return false; - if (!(host.Window is SDL3Window window)) + if (!(host.Window is ISDLWindow window)) return false; Enabled.BindValueChanged(e => diff --git a/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs b/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs index 713d52fcfe..aa480152c3 100644 --- a/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs +++ b/osu.Framework/Input/Handlers/Mouse/MouseHandler.cs @@ -8,7 +8,6 @@ using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.StateChanges; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; using osuTK; using osuTK.Input; @@ -16,7 +15,7 @@ namespace osu.Framework.Input.Handlers.Mouse { /// - /// Handles mouse events from an . + /// Handles mouse events from an . /// Will use relative mouse mode where possible. /// public class MouseHandler : InputHandler, IHasCursorSensitivity, INeedsMousePositionFeedback @@ -42,7 +41,7 @@ public class MouseHandler : InputHandler, IHasCursorSensitivity, INeedsMousePosi public override bool IsActive => true; - private SDL3Window window; + private ISDLWindow window; private Vector2? lastPosition; @@ -77,7 +76,7 @@ public override bool Initialize(GameHost host) if (!base.Initialize(host)) return false; - if (!(host.Window is SDL3Window desktopWindow)) + if (!(host.Window is ISDLWindow desktopWindow)) return false; window = desktopWindow; diff --git a/osu.Framework/Input/Handlers/Touch/TouchHandler.cs b/osu.Framework/Input/Handlers/Touch/TouchHandler.cs index 61eb2dff19..ff0377b8c8 100644 --- a/osu.Framework/Input/Handlers/Touch/TouchHandler.cs +++ b/osu.Framework/Input/Handlers/Touch/TouchHandler.cs @@ -3,7 +3,6 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osu.Framework.Statistics; namespace osu.Framework.Input.Handlers.Touch @@ -19,7 +18,7 @@ public override bool Initialize(GameHost host) if (!base.Initialize(host)) return false; - if (!(host.Window is SDL3Window window)) + if (!(host.Window is ISDLWindow window)) return false; Enabled.BindValueChanged(enabled => diff --git a/osu.Framework/Input/UserInputManager.cs b/osu.Framework/Input/UserInputManager.cs index 7671e41f64..c08f0b2f63 100644 --- a/osu.Framework/Input/UserInputManager.cs +++ b/osu.Framework/Input/UserInputManager.cs @@ -10,7 +10,6 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges.Events; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osuTK; using osuTK.Input; using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; @@ -91,7 +90,7 @@ private bool mouseOutsideAllDisplays(Vector2 mousePosition) switch (Host.Window.WindowMode.Value) { case WindowMode.Windowed: - windowLocation = Host.Window is SDL3Window sdlWindow ? sdlWindow.Position : Point.Empty; + windowLocation = Host.Window is ISDLWindow sdlWindow ? sdlWindow.Position : Point.Empty; break; default: diff --git a/osu.Framework/Platform/ISDLWindow.cs b/osu.Framework/Platform/ISDLWindow.cs new file mode 100644 index 0000000000..7b3112b145 --- /dev/null +++ b/osu.Framework/Platform/ISDLWindow.cs @@ -0,0 +1,37 @@ +// 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.Drawing; +using osu.Framework.Bindables; +using osu.Framework.Input; +using osuTK; +using osuTK.Input; + +namespace osu.Framework.Platform +{ + internal interface ISDLWindow : IWindow + { + event Action JoystickButtonDown; + event Action JoystickButtonUp; + event Action JoystickAxisChanged; + event Action TouchDown; + event Action TouchUp; + event Action MouseMove; + event Action MouseMoveRelative; + event Action MouseDown; + event Action MouseUp; + event Action MouseWheel; + event Action KeyDown; + event Action KeyUp; + + Bindable CursorStateBindable { get; } + + Point Position { get; } + Size Size { get; } + bool MouseAutoCapture { set; } + bool RelativeMouseMode { get; set; } + + void UpdateMousePosition(Vector2 position); + } +} diff --git a/osu.Framework/Platform/SDL2/SDL2Window.cs b/osu.Framework/Platform/SDL2/SDL2Window.cs index 7feff6327f..676501edf4 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window.cs @@ -23,7 +23,7 @@ namespace osu.Framework.Platform.SDL2 /// /// Default implementation of a window, using SDL for windowing and graphics support. /// - internal abstract partial class SDL2Window : IWindow + internal abstract partial class SDL2Window : ISDLWindow { internal IntPtr SDLWindowHandle { get; private set; } = IntPtr.Zero; diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs index 2764dccef7..2432851c53 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs @@ -64,7 +64,7 @@ public bool RelativeMouseMode /// The above culminate in staying off when the cursor leaves and enters the window bounds when any buttons are pressed. /// This is an invalid state, as the cursor is inside the window, and is off. /// - internal bool MouseAutoCapture + public bool MouseAutoCapture { set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1" : "0")); } diff --git a/osu.Framework/Platform/SDL3/SDL3Window.cs b/osu.Framework/Platform/SDL3/SDL3Window.cs index 4801a9891f..d8fc645207 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window.cs @@ -25,7 +25,7 @@ namespace osu.Framework.Platform.SDL3 /// /// Default implementation of a window, using SDL for windowing and graphics support. /// - internal abstract unsafe partial class SDL3Window : IWindow + internal abstract unsafe partial class SDL3Window : ISDLWindow { internal SDL_Window* SDLWindowHandle { get; private set; } = null; diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs index 4e08c3e344..d25d6ad0ff 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -65,7 +65,7 @@ public bool RelativeMouseMode /// The above culminate in staying off when the cursor leaves and enters the window bounds when any buttons are pressed. /// This is an invalid state, as the cursor is inside the window, and is off. /// - internal bool MouseAutoCapture + public bool MouseAutoCapture { set => ScheduleCommand(() => SDL_SetHint(SDL_HINT_MOUSE_AUTO_CAPTURE, value ? "1"u8 : "0"u8)); } From d72212b919f02157087e0ff2b88db4e9e6ec038f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 18:11:10 +0900 Subject: [PATCH 043/140] Merge SDL2GameHost and SDL3GameHost --- ...ndowTextInput.cs => SDLWindowTextInput.cs} | 8 ++-- osu.Framework/Platform/DesktopGameHost.cs | 2 +- osu.Framework/Platform/ISDLWindow.cs | 17 +++++++ .../Platform/SDL2/SDL2Window_Input.cs | 8 ---- osu.Framework/Platform/SDL2GameHost.cs | 48 ------------------- .../Platform/SDL3/SDL3Window_Input.cs | 8 ---- .../{SDL3GameHost.cs => SDLGameHost.cs} | 16 ++++--- 7 files changed, 31 insertions(+), 76 deletions(-) rename osu.Framework/Input/{SDL3WindowTextInput.cs => SDLWindowTextInput.cs} (90%) delete mode 100644 osu.Framework/Platform/SDL2GameHost.cs rename osu.Framework/Platform/{SDL3GameHost.cs => SDLGameHost.cs} (72%) diff --git a/osu.Framework/Input/SDL3WindowTextInput.cs b/osu.Framework/Input/SDLWindowTextInput.cs similarity index 90% rename from osu.Framework/Input/SDL3WindowTextInput.cs rename to osu.Framework/Input/SDLWindowTextInput.cs index 71b450a86f..c0fd539f44 100644 --- a/osu.Framework/Input/SDL3WindowTextInput.cs +++ b/osu.Framework/Input/SDLWindowTextInput.cs @@ -2,15 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Primitives; -using osu.Framework.Platform.SDL3; +using osu.Framework.Platform; namespace osu.Framework.Input { - internal class SDL3WindowTextInput : TextInputSource + internal class SDLWindowTextInput : TextInputSource { - private readonly SDL3Window window; + private readonly ISDLWindow window; - public SDL3WindowTextInput(SDL3Window window) + public SDLWindowTextInput(ISDLWindow window) { this.window = window; } diff --git a/osu.Framework/Platform/DesktopGameHost.cs b/osu.Framework/Platform/DesktopGameHost.cs index 7eb308c2fc..40346e88e0 100644 --- a/osu.Framework/Platform/DesktopGameHost.cs +++ b/osu.Framework/Platform/DesktopGameHost.cs @@ -13,7 +13,7 @@ namespace osu.Framework.Platform { - public abstract class DesktopGameHost : SDL3GameHost + public abstract class DesktopGameHost : SDLGameHost { private TcpIpcProvider ipcProvider; private readonly int? ipcPort; diff --git a/osu.Framework/Platform/ISDLWindow.cs b/osu.Framework/Platform/ISDLWindow.cs index 7b3112b145..6db35cecb4 100644 --- a/osu.Framework/Platform/ISDLWindow.cs +++ b/osu.Framework/Platform/ISDLWindow.cs @@ -7,6 +7,7 @@ using osu.Framework.Input; using osuTK; using osuTK.Input; +using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; namespace osu.Framework.Platform { @@ -24,6 +25,8 @@ internal interface ISDLWindow : IWindow event Action MouseWheel; event Action KeyDown; event Action KeyUp; + event Action TextInput; + event TextEditingDelegate TextEditing; Bindable CursorStateBindable { get; } @@ -31,7 +34,21 @@ internal interface ISDLWindow : IWindow Size Size { get; } bool MouseAutoCapture { set; } bool RelativeMouseMode { get; set; } + bool CapsLockPressed { get; } void UpdateMousePosition(Vector2 position); + + void StartTextInput(bool allowIme); + void StopTextInput(); + void SetTextInputRect(RectangleF rectangle); + void ResetIme(); } + + /// + /// Fired when text is edited, usually via IME composition. + /// + /// The composition text. + /// The index of the selection start. + /// The length of the selection. + public delegate void TextEditingDelegate(string text, int start, int length); } diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs index 2432851c53..43a62dd8ba 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window_Input.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window_Input.cs @@ -653,13 +653,5 @@ private void updateConfineMode() public event Action? TouchUp; #endregion - - /// - /// Fired when text is edited, usually via IME composition. - /// - /// The composition text. - /// The index of the selection start. - /// The length of the selection. - public delegate void TextEditingDelegate(string text, int start, int length); } } diff --git a/osu.Framework/Platform/SDL2GameHost.cs b/osu.Framework/Platform/SDL2GameHost.cs deleted file mode 100644 index b59e2be00a..0000000000 --- a/osu.Framework/Platform/SDL2GameHost.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Framework.Input; -using osu.Framework.Input.Handlers; -using osu.Framework.Input.Handlers.Joystick; -using osu.Framework.Input.Handlers.Keyboard; -using osu.Framework.Input.Handlers.Midi; -using osu.Framework.Input.Handlers.Mouse; -using osu.Framework.Input.Handlers.Tablet; -using osu.Framework.Input.Handlers.Touch; -using osu.Framework.Platform.SDL2; - -namespace osu.Framework.Platform -{ - public abstract class SDL2GameHost : GameHost - { - public override bool CapsLockEnabled => (Window as SDL2Window)?.CapsLockPressed == true; - - protected SDL2GameHost(string gameName, HostOptions? options = null) - : base(gameName, options) - { - } - - protected override TextInputSource CreateTextInput() - { - if (Window is SDL2Window window) - return new SDL2WindowTextInput(window); - - return base.CreateTextInput(); - } - - protected override Clipboard CreateClipboard() => new SDL2Clipboard(); - - protected override IEnumerable CreateAvailableInputHandlers() => - new InputHandler[] - { - new KeyboardHandler(), - // tablet should get priority over mouse to correctly handle cases where tablet drivers report as mice as well. - new OpenTabletDriverHandler(), - new MouseHandler(), - new TouchHandler(), - new JoystickHandler(), - new MidiHandler(), - }; - } -} diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs index d25d6ad0ff..3790ce5b5d 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Input.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Input.cs @@ -648,13 +648,5 @@ private void updateConfineMode() public event Action? TouchUp; #endregion - - /// - /// Fired when text is edited, usually via IME composition. - /// - /// The composition text. - /// The index of the selection start. - /// The length of the selection. - public delegate void TextEditingDelegate(string text, int start, int length); } } diff --git a/osu.Framework/Platform/SDL3GameHost.cs b/osu.Framework/Platform/SDLGameHost.cs similarity index 72% rename from osu.Framework/Platform/SDL3GameHost.cs rename to osu.Framework/Platform/SDLGameHost.cs index 57ef866851..db5b000f60 100644 --- a/osu.Framework/Platform/SDL3GameHost.cs +++ b/osu.Framework/Platform/SDLGameHost.cs @@ -10,30 +10,32 @@ using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Input.Handlers.Tablet; using osu.Framework.Input.Handlers.Touch; +using osu.Framework.Platform.SDL2; using osu.Framework.Platform.SDL3; using SixLabors.ImageSharp.Formats.Png; namespace osu.Framework.Platform { - public abstract class SDL3GameHost : GameHost + public abstract class SDLGameHost : GameHost { - public override bool CapsLockEnabled => (Window as SDL3Window)?.CapsLockPressed == true; + public override bool CapsLockEnabled => (Window as ISDLWindow)?.CapsLockPressed == true; - protected SDL3GameHost(string gameName, HostOptions? options = null) + protected SDLGameHost(string gameName, HostOptions? options = null) : base(gameName, options) { } protected override TextInputSource CreateTextInput() { - if (Window is SDL3Window window) - return new SDL3WindowTextInput(window); + if (Window is ISDLWindow window) + return new SDLWindowTextInput(window); return base.CreateTextInput(); } - // PNG works well on linux - protected override Clipboard CreateClipboard() => new SDL3Clipboard(PngFormat.Instance); + protected override Clipboard CreateClipboard() => Window is SDL3Window + ? new SDL3Clipboard(PngFormat.Instance) // PNG works well on linux + : new SDL2Clipboard(); protected override IEnumerable CreateAvailableInputHandlers() => new InputHandler[] From 1794191b38ea4386f505506c3191f01b392a5541 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 18:34:25 +0900 Subject: [PATCH 044/140] Add SDL2/3 Windows window variants --- osu.Framework/FrameworkEnvironment.cs | 3 + osu.Framework/Platform/ISDLWindow.cs | 1 + .../Platform/Windows/Native/Input.cs | 331 ++++++++++++++++ .../Platform/Windows/SDL2WindowsWindow.cs | 357 ++++++++++++++++++ ...{WindowsWindow.cs => SDL3WindowsWindow.cs} | 8 +- .../Platform/Windows/WindowsGLRenderer.cs | 2 +- .../Platform/Windows/WindowsGameHost.cs | 5 +- 7 files changed, 701 insertions(+), 6 deletions(-) create mode 100644 osu.Framework/Platform/Windows/SDL2WindowsWindow.cs rename osu.Framework/Platform/Windows/{WindowsWindow.cs => SDL3WindowsWindow.cs} (97%) diff --git a/osu.Framework/FrameworkEnvironment.cs b/osu.Framework/FrameworkEnvironment.cs index 8db649e28c..d596ea95d9 100644 --- a/osu.Framework/FrameworkEnvironment.cs +++ b/osu.Framework/FrameworkEnvironment.cs @@ -20,6 +20,7 @@ public static class FrameworkEnvironment public static int? VertexBufferCount { get; } public static bool NoStructuredBuffers { get; } public static string? DeferredRendererEventsOutputPath { get; } + public static bool UseSDL3 { get; } /// /// Whether non-SSL requests should be allowed. Debug only. Defaults to disabled. @@ -51,6 +52,8 @@ static FrameworkEnvironment() if (DebugUtils.IsDebugBuild) AllowInsecureRequests = parseBool(Environment.GetEnvironmentVariable("OSU_INSECURE_REQUESTS")) ?? false; + + UseSDL3 = parseBool(Environment.GetEnvironmentVariable("OSU_SDL3")) ?? false; } private static bool? parseBool(string? value) diff --git a/osu.Framework/Platform/ISDLWindow.cs b/osu.Framework/Platform/ISDLWindow.cs index 6db35cecb4..ab6f9b4087 100644 --- a/osu.Framework/Platform/ISDLWindow.cs +++ b/osu.Framework/Platform/ISDLWindow.cs @@ -27,6 +27,7 @@ internal interface ISDLWindow : IWindow event Action KeyUp; event Action TextInput; event TextEditingDelegate TextEditing; + event Action WindowStateChanged; Bindable CursorStateBindable { get; } diff --git a/osu.Framework/Platform/Windows/Native/Input.cs b/osu.Framework/Platform/Windows/Native/Input.cs index fcb28d5634..851172654f 100644 --- a/osu.Framework/Platform/Windows/Native/Input.cs +++ b/osu.Framework/Platform/Windows/Native/Input.cs @@ -2,12 +2,72 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Drawing; using System.Runtime.InteropServices; namespace osu.Framework.Platform.Windows.Native { internal static class Input { + [DllImport("user32.dll")] + public static extern bool RegisterRawInputDevices( + [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] + RawInputDevice[] pRawInputDevices, + int uiNumDevices, + int cbSize); + + [DllImport("user32.dll")] + public static extern int GetRawInputData(IntPtr hRawInput, RawInputCommand uiCommand, out RawInputData pData, ref int pcbSize, int cbSizeHeader); + + internal static Rectangle VirtualScreenRect => new Rectangle( + GetSystemMetrics(SM_XVIRTUALSCREEN), + GetSystemMetrics(SM_YVIRTUALSCREEN), + GetSystemMetrics(SM_CXVIRTUALSCREEN), + GetSystemMetrics(SM_CYVIRTUALSCREEN)); + + internal const int WM_INPUT = 0x00FF; + + [DllImport("user32.dll")] + public static extern int GetSystemMetrics(int nIndex); + + internal static Rectangle GetVirtualScreenRect() => new Rectangle( + GetSystemMetrics(SM_XVIRTUALSCREEN), + GetSystemMetrics(SM_YVIRTUALSCREEN), + GetSystemMetrics(SM_CXVIRTUALSCREEN), + GetSystemMetrics(SM_CYVIRTUALSCREEN) + ); + + public const int SM_XVIRTUALSCREEN = 76; + public const int SM_YVIRTUALSCREEN = 77; + public const int SM_CXVIRTUALSCREEN = 78; + public const int SM_CYVIRTUALSCREEN = 79; + + public const long MI_WP_SIGNATURE = 0xFF515700; + public const long MI_WP_SIGNATURE_MASK = 0xFFFFFF00; + + /// + /// Flag distinguishing touch input from mouse input in events. + /// + /// + /// https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages + /// Additionally, the eighth bit, masked by 0x80, is used to differentiate touch input from pen input (0 = pen, 1 = touch). + /// + private const long touch_flag = 0x80; + + /// + /// https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages + /// + /// for the current event. + /// true if this event is from a finger touch, false if it's from mouse or pen input. + public static bool IsTouchEvent(long dw) => (dw & MI_WP_SIGNATURE_MASK) == MI_WP_SIGNATURE && HasTouchFlag(dw); + + /// or + /// Whether has the set. + public static bool HasTouchFlag(long extraInformation) => (extraInformation & touch_flag) == touch_flag; + + [DllImport("user32.dll", SetLastError = false)] + public static extern long GetMessageExtraInfo(); + [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern unsafe bool SetWindowFeedbackSetting(IntPtr hwnd, FeedbackType feedback, ulong flags, uint size, int* configuration); @@ -27,6 +87,277 @@ public static unsafe void SetWindowFeedbackSetting(IntPtr hwnd, FeedbackType fee } } + /// + /// Value type for raw input. + /// + public struct RawInputData + { + /// Header for the data. + public RawInputHeader Header; + + /// Mouse raw input data. + public RawMouse Mouse; + + // This struct is a lot larger but the remaining elements have been omitted until required (Keyboard / HID / Touch). + } + + /// + /// Contains information about the state of the mouse. + /// + public struct RawMouse + { + /// + /// The mouse state. + /// + public RawMouseFlags Flags; + + /// + /// Flags for the event. + /// + public RawMouseButtons ButtonFlags; + + /// + /// If the mouse wheel is moved, this will contain the delta amount. + /// + public short ButtonData; + + /// + /// Raw button data. + /// + public uint RawButtons; + + /// + /// The motion in the X direction. This is signed relative motion or + /// absolute motion, depending on the value of usFlags. + /// + public int LastX; + + /// + /// The motion in the Y direction. This is signed relative motion or absolute motion, + /// depending on the value of usFlags. + /// + public int LastY; + + /// + /// The device-specific additional information for the event. + /// + public uint ExtraInformation; + } + + /// + /// Enumeration containing the flags for raw mouse data. + /// + [Flags] + public enum RawMouseFlags : ushort + { + /// Relative to the last position. + MoveRelative = 0, + + /// Absolute positioning. + MoveAbsolute = 1, + + /// Coordinate data is mapped to a virtual desktop. + VirtualDesktop = 2, + + /// Attributes for the mouse have changed. + AttributesChanged = 4, + + /// WM_MOUSEMOVE and WM_INPUT don't coalesce + MoveNoCoalesce = 8, + } + + /// + /// Enumeration containing the button data for raw mouse input. + /// + public enum RawMouseButtons : ushort + { + /// No button. + None = 0, + + /// Left (button 1) down. + LeftDown = 0x0001, + + /// Left (button 1) up. + LeftUp = 0x0002, + + /// Right (button 2) down. + RightDown = 0x0004, + + /// Right (button 2) up. + RightUp = 0x0008, + + /// Middle (button 3) down. + MiddleDown = 0x0010, + + /// Middle (button 3) up. + MiddleUp = 0x0020, + + /// Button 4 down. + Button4Down = 0x0040, + + /// Button 4 up. + Button4Up = 0x0080, + + /// Button 5 down. + Button5Down = 0x0100, + + /// Button 5 up. + Button5Up = 0x0200, + + /// Mouse wheel moved. + MouseWheel = 0x0400 + } + + /// + /// Enumeration contanining the command types to issue. + /// + public enum RawInputCommand + { + /// + /// Get input data. + /// + Input = 0x10000003, + + /// + /// Get header data. + /// + Header = 0x10000005 + } + + /// + /// Enumeration containing the type device the raw input is coming from. + /// + public enum RawInputType + { + /// + /// Mouse input. + /// + Mouse = 0, + + /// + /// Keyboard input. + /// + Keyboard = 1, + + /// + /// Another device that is not the keyboard or the mouse. + /// + HID = 2 + } + +#pragma warning disable IDE1006 // Naming style + + /// + /// Value type for a raw input header. + /// + [StructLayout(LayoutKind.Sequential)] + public struct RawInputHeader + { + /// Type of device the input is coming from. + public RawInputType Type; + + /// Size of the packet of data. + public int Size; + + /// Handle to the device sending the data. + public IntPtr Device; + + /// wParam from the window message. + public IntPtr wParam; + } + + [StructLayout(LayoutKind.Sequential)] + public struct RawInputDevice + { + /// Top level collection Usage page for the raw input device. + public HIDUsagePage UsagePage; + + /// Top level collection Usage for the raw input device. + public HIDUsage Usage; + + /// Mode flag that specifies how to interpret the information provided by UsagePage and Usage. + public RawInputDeviceFlags Flags; + + /// Handle to the target device. If NULL, it follows the keyboard focus. + public IntPtr WindowHandle; + } + +#pragma warning restore IDE1006 + + /// Enumeration containing flags for a raw input device. + public enum RawInputDeviceFlags + { + /// No flags. + None = 0, + + /// If set, this removes the top level collection from the inclusion list. This tells the operating system to stop reading from a device which matches the top level collection. + Remove = 0x00000001, + + /// If set, this specifies the top level collections to exclude when reading a complete usage page. This flag only affects a TLC whose usage page is already specified with PageOnly. + Exclude = 0x00000010, + + /// If set, this specifies all devices whose top level collection is from the specified usUsagePage. Note that Usage must be zero. To exclude a particular top level collection, use Exclude. + PageOnly = 0x00000020, + + /// If set, this prevents any devices specified by UsagePage or Usage from generating legacy messages. This is only for the mouse and keyboard. + NoLegacy = 0x00000030, + + /// If set, this enables the caller to receive the input even when the caller is not in the foreground. Note that WindowHandle must be specified. + InputSink = 0x00000100, + + /// If set, the mouse button click does not activate the other window. + CaptureMouse = 0x00000200, + + /// If set, the application-defined keyboard device hotkeys are not handled. However, the system hotkeys; for example, ALT+TAB and CTRL+ALT+DEL, are still handled. By default, all keyboard hotkeys are handled. NoHotKeys can be specified even if NoLegacy is not specified and WindowHandle is NULL. + NoHotKeys = 0x00000200, + + /// If set, application keys are handled. NoLegacy must be specified. Keyboard only. + AppKeys = 0x00000400 + } + + public enum HIDUsage : ushort + { + Pointer = 0x01, + Mouse = 0x02, + Joystick = 0x04, + Gamepad = 0x05, + Keyboard = 0x06, + Keypad = 0x07, + SystemControl = 0x80, + } + + public enum HIDUsagePage : ushort + { + Undefined = 0x00, + Generic = 0x01, + Simulation = 0x02, + VR = 0x03, + Sport = 0x04, + Game = 0x05, + Keyboard = 0x07, + LED = 0x08, + Button = 0x09, + Ordinal = 0x0A, + Telephony = 0x0B, + Consumer = 0x0C, + Digitizer = 0x0D, + PID = 0x0F, + Unicode = 0x10, + AlphaNumeric = 0x14, + Medical = 0x40, + MonitorPage0 = 0x80, + MonitorPage1 = 0x81, + MonitorPage2 = 0x82, + MonitorPage3 = 0x83, + PowerPage0 = 0x84, + PowerPage1 = 0x85, + PowerPage2 = 0x86, + PowerPage3 = 0x87, + BarCode = 0x8C, + Scale = 0x8D, + MSR = 0x8E + } + public enum FeedbackType { TouchContactVisualization = 1, diff --git a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs new file mode 100644 index 0000000000..0494d54814 --- /dev/null +++ b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs @@ -0,0 +1,357 @@ +// 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.Drawing; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Platform.SDL2; +using osu.Framework.Platform.Windows.Native; +using osuTK; +using osuTK.Input; +using Icon = osu.Framework.Platform.Windows.Native.Icon; +using static SDL2.SDL; + +namespace osu.Framework.Platform.Windows +{ + [SupportedOSPlatform("windows")] + internal class SDL2WindowsWindow : SDL2DesktopWindow + { + private const int seticon_message = 0x0080; + private const int icon_big = 1; + private const int icon_small = 0; + + private const int large_icon_size = 256; + private const int small_icon_size = 16; + + private Icon? smallIcon; + private Icon? largeIcon; + + private const int wm_killfocus = 8; + + /// + /// Whether to apply the . + /// + private readonly bool applyBorderlessWindowHack; + + public SDL2WindowsWindow(GraphicsSurfaceType surfaceType, string appName) + : base(surfaceType, appName) + { + switch (surfaceType) + { + case GraphicsSurfaceType.OpenGL: + case GraphicsSurfaceType.Vulkan: + applyBorderlessWindowHack = true; + break; + + case GraphicsSurfaceType.Direct3D11: + applyBorderlessWindowHack = false; + break; + } + + if (!declareDpiAwareV2()) + declareDpiAware(); + } + + private bool declareDpiAwareV2() + { + try + { + return SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } + catch + { + return false; + } + } + + private bool declareDpiAware() + { + try + { + return SetProcessDpiAwareness(ProcessDpiAwareness.Process_Per_Monitor_DPI_Aware); + } + catch + { + return false; + } + } + + public override void Create() + { + base.Create(); + + // disable all pen and touch feedback as this causes issues when running "optimised" fullscreen under Direct3D11. + foreach (var feedbackType in Enum.GetValues()) + Native.Input.SetWindowFeedbackSetting(WindowHandle, feedbackType, false); + + // enable window message events to use with `OnSDLEvent` below. + SDL_EventState(SDL_EventType.SDL_SYSWMEVENT, SDL_ENABLE); + } + + protected override void HandleEventFromFilter(SDL_Event e) + { + if (e.type == SDL_EventType.SDL_SYSWMEVENT) + { + var wmMsg = Marshal.PtrToStructure(e.syswm.msg); + var m = wmMsg.msg.win; + + switch (m.msg) + { + case wm_killfocus: + warpCursorFromFocusLoss(); + break; + + case Imm.WM_IME_STARTCOMPOSITION: + case Imm.WM_IME_COMPOSITION: + case Imm.WM_IME_ENDCOMPOSITION: + handleImeMessage(m.hwnd, m.msg, m.lParam); + break; + } + } + + base.HandleEventFromFilter(e); + } + + /// + /// The last mouse position as reported by . + /// + internal Vector2? LastMousePosition { private get; set; } + + /// + /// If required, warps the OS cursor to match the framework cursor position. + /// + /// + /// The normal warp in doesn't work in fullscreen, + /// as it is called when the window has already lost focus and is minimized. + /// So we do an out-of-band warp, immediately after receiving the message. + /// + private void warpCursorFromFocusLoss() + { + if (LastMousePosition.HasValue + && WindowMode.Value == Configuration.WindowMode.Fullscreen + && RelativeMouseMode) + { + var pt = PointToScreen(new Point((int)LastMousePosition.Value.X, (int)LastMousePosition.Value.Y)); + SDL_WarpMouseGlobal(pt.X, pt.Y); // this directly calls the SetCursorPos win32 API + } + } + + #region IME handling + + public override void StartTextInput(bool allowIme) + { + base.StartTextInput(allowIme); + ScheduleCommand(() => Imm.SetImeAllowed(WindowHandle, allowIme)); + } + + public override void ResetIme() => ScheduleCommand(() => Imm.CancelComposition(WindowHandle)); + + protected override unsafe void HandleTextInputEvent(SDL_TextInputEvent evtText) + { + if (!SDL2Extensions.TryGetStringFromBytePointer(evtText.text, out string sdlResult)) + return; + + // Block SDL text input if it was already handled by `handleImeMessage()`. + // SDL truncates text over 32 bytes and sends it as multiple events. + // We assume these events will be handled in the same `pollSDLEvents()` call. + if (lastImeResult?.Contains(sdlResult) == true) + { + // clear the result after this SDL event loop finishes so normal text input isn't blocked. + EventScheduler.AddOnce(() => lastImeResult = null); + return; + } + + // also block if there is an ongoing composition (unlikely to occur). + if (imeCompositionActive) return; + + base.HandleTextInputEvent(evtText); + } + + protected override void HandleTextEditingEvent(SDL_TextEditingEvent evtEdit) + { + // handled by custom logic below + } + + /// + /// Whether IME composition is active. + /// + /// Used for blocking SDL IME results since we handle those ourselves. + private bool imeCompositionActive; + + /// + /// The last IME result. + /// + /// + /// Used for blocking SDL IME results since we handle those ourselves. + /// Cleared when the SDL events are blocked. + /// + private string? lastImeResult; + + private void handleImeMessage(IntPtr hWnd, uint uMsg, long lParam) + { + switch (uMsg) + { + case Imm.WM_IME_STARTCOMPOSITION: + imeCompositionActive = true; + ScheduleEvent(() => TriggerTextEditing(string.Empty, 0, 0)); + break; + + case Imm.WM_IME_COMPOSITION: + using (var inputContext = new Imm.InputContext(hWnd, lParam)) + { + if (inputContext.TryGetImeResult(out string? resultText)) + { + lastImeResult = resultText; + ScheduleEvent(() => TriggerTextInput(resultText)); + } + + if (inputContext.TryGetImeComposition(out string? compositionText, out int start, out int length)) + { + ScheduleEvent(() => TriggerTextEditing(compositionText, start, length)); + } + } + + break; + + case Imm.WM_IME_ENDCOMPOSITION: + imeCompositionActive = false; + ScheduleEvent(() => TriggerTextEditing(string.Empty, 0, 0)); + break; + } + } + + #endregion + + protected override void HandleTouchFingerEvent(SDL_TouchFingerEvent evtTfinger) + { + if (evtTfinger.TryGetTouchName(out string name) && name == "pen") + { + // Windows Ink tablet/pen handling + // InputManager expects to receive this as mouse events, to have proper `mouseSource` input priority (see InputManager.GetPendingInputs) + // osu! expects to get tablet events as mouse events, and touch events as touch events for touch device (TD mod) handling (see https://github.com/ppy/osu/issues/25590) + + TriggerMouseMove(evtTfinger.x * ClientSize.Width, evtTfinger.y * ClientSize.Height); + + switch (evtTfinger.type) + { + case SDL_EventType.SDL_FINGERDOWN: + TriggerMouseDown(MouseButton.Left); + break; + + case SDL_EventType.SDL_FINGERUP: + TriggerMouseUp(MouseButton.Left); + break; + } + + return; + } + + base.HandleTouchFingerEvent(evtTfinger); + } + + public override Size Size + { + protected set + { + // trick the game into thinking the borderless window has normal size so that it doesn't render into the extra space. + if (applyBorderlessWindowHack && WindowState == WindowState.FullscreenBorderless) + value.Width -= windows_borderless_width_hack; + + base.Size = value; + } + } + + /// + /// Amount of extra width added to window size when in borderless mode on Windows. + /// Some drivers require this to avoid the window switching to exclusive fullscreen automatically. + /// + /// Used on and . + private const int windows_borderless_width_hack = 1; + + protected override Size SetBorderless(Display display) + { + SDL_SetWindowBordered(SDLWindowHandle, SDL_bool.SDL_FALSE); + + var newSize = display.Bounds.Size; + + if (applyBorderlessWindowHack) + // use the 1px hack we've always used, but only expand the width. + // we also trick the game into thinking the window has normal size: see Size setter override + newSize += new Size(windows_borderless_width_hack, 0); + + SDL_SetWindowSize(SDLWindowHandle, newSize.Width, newSize.Height); + Position = display.Bounds.Location; + + return newSize; + } + + /// + /// On Windows, SDL will use the same image for both large and small icons (scaled as necessary). + /// This can look bad if scaling down a large image, so we use the Windows API directly so as + /// to get a cleaner icon set than SDL can provide. + /// If called before the window has been created, or we do not find two separate icon sizes, we fall back to the base method. + /// + internal override void SetIconFromGroup(IconGroup iconGroup) + { + smallIcon = iconGroup.CreateIcon(small_icon_size, small_icon_size); + largeIcon = iconGroup.CreateIcon(large_icon_size, large_icon_size); + + IntPtr windowHandle = WindowHandle; + + if (windowHandle == IntPtr.Zero || largeIcon == null || smallIcon == null) + base.SetIconFromGroup(iconGroup); + else + { + SendMessage(windowHandle, seticon_message, icon_small, smallIcon.Handle); + SendMessage(windowHandle, seticon_message, icon_big, largeIcon.Handle); + } + } + + public override Point PointToClient(Point point) + { + ScreenToClient(WindowHandle, ref point); + return point; + } + + public override Point PointToScreen(Point point) + { + ClientToScreen(WindowHandle, ref point); + return point; + } + + [DllImport("SHCore.dll", SetLastError = true)] + internal static extern bool SetProcessDpiAwareness(ProcessDpiAwareness awareness); + + internal enum ProcessDpiAwareness + { + Process_DPI_Unaware = 0, + Process_System_DPI_Aware = 1, + Process_Per_Monitor_DPI_Aware = 2 + } + + [DllImport("User32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT value); + + // ReSharper disable once InconsistentNaming + internal enum DPI_AWARENESS_CONTEXT + { + DPI_AWARENESS_CONTEXT_UNAWARE = -1, + DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4, + DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = -5, + } + + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool ScreenToClient(IntPtr hWnd, ref Point point); + + [DllImport("user32.dll", SetLastError = true)] + internal static extern bool ClientToScreen(IntPtr hWnd, ref Point point); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + } +} diff --git a/osu.Framework/Platform/Windows/WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs similarity index 97% rename from osu.Framework/Platform/Windows/WindowsWindow.cs rename to osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index f0b3171474..36b96269ec 100644 --- a/osu.Framework/Platform/Windows/WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -20,7 +20,7 @@ namespace osu.Framework.Platform.Windows { [SupportedOSPlatform("windows")] - internal class WindowsWindow : SDL3DesktopWindow + internal class SDL3WindowsWindow : SDL3DesktopWindow { private const int seticon_message = 0x0080; private const int icon_big = 1; @@ -37,7 +37,7 @@ internal class WindowsWindow : SDL3DesktopWindow /// private readonly bool applyBorderlessWindowHack; - public WindowsWindow(GraphicsSurfaceType surfaceType, string appName) + public SDL3WindowsWindow(GraphicsSurfaceType surfaceType, string appName) : base(surfaceType, appName) { switch (surfaceType) @@ -71,8 +71,8 @@ public override unsafe void Run() [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] private static unsafe SDL_bool messageHook(IntPtr userdata, MSG* msg) { - var handle = new ObjectHandle(userdata); - if (handle.GetTarget(out WindowsWindow window)) + var handle = new ObjectHandle(userdata); + if (handle.GetTarget(out SDL3WindowsWindow window)) return window.handleEventFromHook(*msg); return SDL_bool.SDL_TRUE; diff --git a/osu.Framework/Platform/Windows/WindowsGLRenderer.cs b/osu.Framework/Platform/Windows/WindowsGLRenderer.cs index d52f2f67a5..aee3e5c410 100644 --- a/osu.Framework/Platform/Windows/WindowsGLRenderer.cs +++ b/osu.Framework/Platform/Windows/WindowsGLRenderer.cs @@ -28,7 +28,7 @@ protected override void Initialise(IGraphicsSurface graphicsSurface) { base.Initialise(graphicsSurface); - WindowsWindow windowsWindow = (WindowsWindow)host.Window; + ISDLWindow windowsWindow = (ISDLWindow)host.Window; bool isIntel = GL.GetString(StringName.Vendor).Trim() == "Intel"; diff --git a/osu.Framework/Platform/Windows/WindowsGameHost.cs b/osu.Framework/Platform/Windows/WindowsGameHost.cs index 0f90cfcaa9..f27fd1c4d7 100644 --- a/osu.Framework/Platform/Windows/WindowsGameHost.cs +++ b/osu.Framework/Platform/Windows/WindowsGameHost.cs @@ -86,7 +86,10 @@ protected override void SetupForRun() timePeriod = new TimePeriod(1); } - protected override IWindow CreateWindow(GraphicsSurfaceType preferredSurface) => new WindowsWindow(preferredSurface, Options.FriendlyGameName); + protected override IWindow CreateWindow(GraphicsSurfaceType preferredSurface) + => FrameworkEnvironment.UseSDL3 + ? new SDL3WindowsWindow(preferredSurface, Options.FriendlyGameName) + : new SDL2WindowsWindow(preferredSurface, Options.FriendlyGameName); public override IEnumerable PlatformKeyBindings => base.PlatformKeyBindings.Concat(new[] { From 14ca6826f19368d83b34b49010f74fbac051301f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 18:34:44 +0900 Subject: [PATCH 045/140] Add SDL2/3 Linux window variants --- osu.Framework/Platform/Linux/LinuxGameHost.cs | 13 ++++--------- osu.Framework/Platform/Linux/SDL2LinuxWindow.cs | 17 +++++++++++++++++ osu.Framework/Platform/Linux/SDL3LinuxWindow.cs | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 9 deletions(-) create mode 100644 osu.Framework/Platform/Linux/SDL2LinuxWindow.cs create mode 100644 osu.Framework/Platform/Linux/SDL3LinuxWindow.cs diff --git a/osu.Framework/Platform/Linux/LinuxGameHost.cs b/osu.Framework/Platform/Linux/LinuxGameHost.cs index fb08932dd6..eb8278e6b6 100644 --- a/osu.Framework/Platform/Linux/LinuxGameHost.cs +++ b/osu.Framework/Platform/Linux/LinuxGameHost.cs @@ -6,8 +6,6 @@ using osu.Framework.Input; using osu.Framework.Input.Handlers; using osu.Framework.Input.Handlers.Mouse; -using osu.Framework.Platform.SDL3; -using static SDL.SDL3; namespace osu.Framework.Platform.Linux { @@ -28,13 +26,10 @@ internal LinuxGameHost(string gameName, HostOptions? options) BypassCompositor = Options.BypassCompositor; } - protected override void SetupForRun() - { - SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, BypassCompositor ? "1"u8 : "0"u8); - base.SetupForRun(); - } - - protected override IWindow CreateWindow(GraphicsSurfaceType preferredSurface) => new SDL3DesktopWindow(preferredSurface, Options.FriendlyGameName); + protected override IWindow CreateWindow(GraphicsSurfaceType preferredSurface) + => FrameworkEnvironment.UseSDL3 + ? new SDL3LinuxWindow(preferredSurface, Options.FriendlyGameName, BypassCompositor) + : new SDL2LinuxWindow(preferredSurface, Options.FriendlyGameName, BypassCompositor); protected override ReadableKeyCombinationProvider CreateReadableKeyCombinationProvider() => new LinuxReadableKeyCombinationProvider(); diff --git a/osu.Framework/Platform/Linux/SDL2LinuxWindow.cs b/osu.Framework/Platform/Linux/SDL2LinuxWindow.cs new file mode 100644 index 0000000000..c28f85a70a --- /dev/null +++ b/osu.Framework/Platform/Linux/SDL2LinuxWindow.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform.SDL2; +using static SDL2.SDL; + +namespace osu.Framework.Platform.Linux +{ + internal class SDL2LinuxWindow : SDL2DesktopWindow + { + public SDL2LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor) + : base(surfaceType, appName) + { + SDL_SetHint(SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, bypassCompositor ? "1" : "0"); + } + } +} diff --git a/osu.Framework/Platform/Linux/SDL3LinuxWindow.cs b/osu.Framework/Platform/Linux/SDL3LinuxWindow.cs new file mode 100644 index 0000000000..2195f38fe3 --- /dev/null +++ b/osu.Framework/Platform/Linux/SDL3LinuxWindow.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Platform.SDL3; +using static SDL.SDL3; + +namespace osu.Framework.Platform.Linux +{ + internal class SDL3LinuxWindow : SDL3DesktopWindow + { + public SDL3LinuxWindow(GraphicsSurfaceType surfaceType, string appName, bool bypassCompositor) + : base(surfaceType, appName) + { + SDL_SetHint(SDL_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR, bypassCompositor ? "1"u8 : "0"u8); + } + } +} From 0a220b469e3d2595dcbc54fe50031f6b4f33d40b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 18:34:55 +0900 Subject: [PATCH 046/140] Add SDL2/3 macOS window variants --- osu.Framework/Platform/MacOS/MacOSGameHost.cs | 5 +- .../Platform/MacOS/SDL2MacOSWindow.cs | 71 +++++++++++++++++++ .../{MacOSWindow.cs => SDL3MacOSWindow.cs} | 4 +- 3 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs rename osu.Framework/Platform/MacOS/{MacOSWindow.cs => SDL3MacOSWindow.cs} (95%) diff --git a/osu.Framework/Platform/MacOS/MacOSGameHost.cs b/osu.Framework/Platform/MacOS/MacOSGameHost.cs index e438b54d1a..1d9d3c6127 100644 --- a/osu.Framework/Platform/MacOS/MacOSGameHost.cs +++ b/osu.Framework/Platform/MacOS/MacOSGameHost.cs @@ -21,7 +21,10 @@ internal MacOSGameHost(string gameName, HostOptions options) { } - protected override IWindow CreateWindow(GraphicsSurfaceType preferredSurface) => new MacOSWindow(preferredSurface, Options.FriendlyGameName); + protected override IWindow CreateWindow(GraphicsSurfaceType preferredSurface) + => FrameworkEnvironment.UseSDL3 + ? new SDL3MacOSWindow(preferredSurface, Options.FriendlyGameName) + : new SDL2MacOSWindow(preferredSurface, Options.FriendlyGameName); public override IEnumerable UserStoragePaths { diff --git a/osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs b/osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs new file mode 100644 index 0000000000..eedf6a5625 --- /dev/null +++ b/osu.Framework/Platform/MacOS/SDL2MacOSWindow.cs @@ -0,0 +1,71 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable disable + +using System; +using osu.Framework.Platform.MacOS.Native; +using osu.Framework.Platform.SDL2; +using osuTK; + +namespace osu.Framework.Platform.MacOS +{ + /// + /// macOS-specific subclass of . + /// + internal class SDL2MacOSWindow : SDL2DesktopWindow + { + private static readonly IntPtr sel_hasprecisescrollingdeltas = Selector.Get("hasPreciseScrollingDeltas"); + private static readonly IntPtr sel_scrollingdeltax = Selector.Get("scrollingDeltaX"); + private static readonly IntPtr sel_scrollingdeltay = Selector.Get("scrollingDeltaY"); + private static readonly IntPtr sel_respondstoselector_ = Selector.Get("respondsToSelector:"); + + private delegate void ScrollWheelDelegate(IntPtr handle, IntPtr selector, IntPtr theEvent); // v@:@ + + private IntPtr originalScrollWheel; + private ScrollWheelDelegate scrollWheelHandler; + + public SDL2MacOSWindow(GraphicsSurfaceType surfaceType, string appName) + : base(surfaceType, appName) + { + } + + public override void Create() + { + base.Create(); + + // replace [SDLView scrollWheel:(NSEvent *)] with our own version + IntPtr viewClass = Class.Get("SDLView"); + scrollWheelHandler = scrollWheel; + originalScrollWheel = Class.SwizzleMethod(viewClass, "scrollWheel:", "v@:@", scrollWheelHandler); + } + + /// + /// Swizzled replacement of [SDLView scrollWheel:(NSEvent *)] that checks for precise scrolling deltas. + /// + private void scrollWheel(IntPtr receiver, IntPtr selector, IntPtr theEvent) + { + bool hasPrecise = Cocoa.SendBool(theEvent, sel_respondstoselector_, sel_hasprecisescrollingdeltas) && + Cocoa.SendBool(theEvent, sel_hasprecisescrollingdeltas); + + if (!hasPrecise) + { + // calls the unswizzled [SDLView scrollWheel:(NSEvent *)] method if this is a regular scroll wheel event + // the receiver may sometimes not be SDLView, ensure it has a scroll wheel selector implemented before attempting to call. + if (Cocoa.SendBool(receiver, sel_respondstoselector_, originalScrollWheel)) + Cocoa.SendVoid(receiver, originalScrollWheel, theEvent); + + return; + } + + // according to osuTK, 0.1f is the scaling factor expected to be returned by CGEventSourceGetPixelsPerLine + // this is additionally scaled down by a factor of 8 so that a precise scroll of 1.0 is roughly equivalent to one notch on a traditional scroll wheel. + const float scale_factor = 0.1f / 8; + + float scrollingDeltaX = Cocoa.SendFloat(theEvent, sel_scrollingdeltax); + float scrollingDeltaY = Cocoa.SendFloat(theEvent, sel_scrollingdeltay); + + ScheduleEvent(() => TriggerMouseWheel(new Vector2(scrollingDeltaX * scale_factor, scrollingDeltaY * scale_factor), true)); + } + } +} diff --git a/osu.Framework/Platform/MacOS/MacOSWindow.cs b/osu.Framework/Platform/MacOS/SDL3MacOSWindow.cs similarity index 95% rename from osu.Framework/Platform/MacOS/MacOSWindow.cs rename to osu.Framework/Platform/MacOS/SDL3MacOSWindow.cs index 98b140d34b..a316eaf640 100644 --- a/osu.Framework/Platform/MacOS/MacOSWindow.cs +++ b/osu.Framework/Platform/MacOS/SDL3MacOSWindow.cs @@ -13,7 +13,7 @@ namespace osu.Framework.Platform.MacOS /// /// macOS-specific subclass of . /// - internal class MacOSWindow : SDL3DesktopWindow + internal class SDL3MacOSWindow : SDL3DesktopWindow { private static readonly IntPtr sel_hasprecisescrollingdeltas = Selector.Get("hasPreciseScrollingDeltas"); private static readonly IntPtr sel_scrollingdeltax = Selector.Get("scrollingDeltaX"); @@ -25,7 +25,7 @@ internal class MacOSWindow : SDL3DesktopWindow private IntPtr originalScrollWheel; private ScrollWheelDelegate scrollWheelHandler; - public MacOSWindow(GraphicsSurfaceType surfaceType, string appName) + public SDL3MacOSWindow(GraphicsSurfaceType surfaceType, string appName) : base(surfaceType, appName) { } From 4aae23e16ef032de981d68ee84e89d6fe218ae40 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 18:45:18 +0900 Subject: [PATCH 047/140] Add SDL2/3 Windows mouse handler variants --- .../Windows/SDL2WindowsMouseHandler.cs | 158 ++++++++++++++++++ .../Platform/Windows/SDL2WindowsWindow.cs | 2 +- ...eHandler.cs => SDL3WindowsMouseHandler.cs} | 6 +- .../Platform/Windows/SDL3WindowsWindow.cs | 2 +- .../Platform/Windows/WindowsGameHost.cs | 7 +- 5 files changed, 169 insertions(+), 6 deletions(-) create mode 100644 osu.Framework/Platform/Windows/SDL2WindowsMouseHandler.cs rename osu.Framework/Platform/Windows/{WindowsMouseHandler.cs => SDL3WindowsMouseHandler.cs} (84%) diff --git a/osu.Framework/Platform/Windows/SDL2WindowsMouseHandler.cs b/osu.Framework/Platform/Windows/SDL2WindowsMouseHandler.cs new file mode 100644 index 0000000000..281d4fc9f0 --- /dev/null +++ b/osu.Framework/Platform/Windows/SDL2WindowsMouseHandler.cs @@ -0,0 +1,158 @@ +// 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.Drawing; +using System.Runtime.Versioning; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Input.Handlers.Mouse; +using osu.Framework.Input.StateChanges; +using osu.Framework.Platform.Windows.Native; +using osu.Framework.Statistics; +using osuTK; +using static SDL2.SDL; + +namespace osu.Framework.Platform.Windows +{ + /// + /// A windows specific mouse input handler which overrides the SDL2 implementation of raw input. + /// This is done to better handle quirks of some devices. + /// + [SupportedOSPlatform("windows")] + internal unsafe class SDL2WindowsMouseHandler : MouseHandler + { + private static readonly GlobalStatistic statistic_relative_events = GlobalStatistics.Get(StatisticGroupFor(), "Relative events"); + private static readonly GlobalStatistic statistic_absolute_events = GlobalStatistics.Get(StatisticGroupFor(), "Absolute events"); + private static readonly GlobalStatistic statistic_dropped_touch_inputs = GlobalStatistics.Get(StatisticGroupFor(), "Dropped native touch inputs"); + + private static readonly GlobalStatistic statistic_inputs_with_extra_information = + GlobalStatistics.Get(StatisticGroupFor(), "Native inputs with ExtraInformation"); + + private const int raw_input_coordinate_space = 65535; + + private SDL_WindowsMessageHook callback = null!; + private SDL2WindowsWindow window = null!; + + public override bool IsActive => Enabled.Value; + + public override bool Initialize(GameHost host) + { + if (!(host.Window is SDL2WindowsWindow desktopWindow)) + return false; + + window = desktopWindow; + // ReSharper disable once ConvertClosureToMethodGroup + callback = (ptr, wnd, u, param, l) => onWndProc(ptr, wnd, u, param, l); + + Enabled.BindValueChanged(enabled => + { + host.InputThread.Scheduler.Add(() => SDL_SetWindowsMessageHook(enabled.NewValue ? callback : null, IntPtr.Zero)); + }, true); + + return base.Initialize(host); + } + + public override void FeedbackMousePositionChange(Vector2 position, bool isSelfFeedback) + { + window.LastMousePosition = position; + base.FeedbackMousePositionChange(position, isSelfFeedback); + } + + protected override void HandleMouseMoveRelative(Vector2 delta) + { + // handled via custom logic below. + } + + private IntPtr onWndProc(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) + { + if (!Enabled.Value) + return IntPtr.Zero; + + if (message != Native.Input.WM_INPUT) + return IntPtr.Zero; + + if (Native.Input.IsTouchEvent(Native.Input.GetMessageExtraInfo())) + { + // sometimes GetMessageExtraInfo returns 0, so additionally, mouse.ExtraInformation is checked below. + // touch events are handled by TouchHandler + statistic_dropped_touch_inputs.Value++; + return IntPtr.Zero; + } + + int payloadSize = sizeof(RawInputData); + + Native.Input.GetRawInputData((IntPtr)lParam, RawInputCommand.Input, out var data, ref payloadSize, sizeof(RawInputHeader)); + + if (data.Header.Type != RawInputType.Mouse) + return IntPtr.Zero; + + var mouse = data.Mouse; + + // `ExtraInformation` doesn't have the MI_WP_SIGNATURE set, so we have to rely solely on the touch flag. + if (Native.Input.HasTouchFlag(mouse.ExtraInformation)) + { + statistic_dropped_touch_inputs.Value++; + return IntPtr.Zero; + } + + //TODO: this isn't correct. + if (mouse.ExtraInformation > 0) + { + statistic_inputs_with_extra_information.Value++; + + // i'm not sure if there is a valid case where we need to handle packets with this present + // but the osu!tablet fires noise events with non-zero values, which we want to ignore. + // return IntPtr.Zero; + } + + var position = new Vector2(mouse.LastX, mouse.LastY); + float sensitivity = (float)Sensitivity.Value; + + if (mouse.Flags.HasFlagFast(RawMouseFlags.MoveAbsolute)) + { + var screenRect = mouse.Flags.HasFlagFast(RawMouseFlags.VirtualDesktop) ? Native.Input.VirtualScreenRect : new Rectangle(window.Position, window.ClientSize); + + Vector2 screenSize = new Vector2(screenRect.Width, screenRect.Height); + + if (mouse.LastX == 0 && mouse.LastY == 0) + { + // not sure if this is the case for all tablets, but on osu!tablet these can appear and are noise. + return IntPtr.Zero; + } + + // i am not sure what this 64 flag is, but it's set on the osu!tablet at very least. + // using it here as a method of determining where the coordinate space is incorrect. + if (((int)mouse.Flags & 64) == 0) + { + position /= raw_input_coordinate_space; + position *= screenSize; + } + + if (Sensitivity.Value != 1) + { + // apply absolute sensitivity adjustment from the centre of the screen area. + Vector2 halfScreenSize = (screenSize / 2); + + position -= halfScreenSize; + position *= (float)Sensitivity.Value; + position += halfScreenSize; + } + + // map from screen to client coordinate space. + // not using Window's PointToClient implementation to keep floating point precision here. + position -= new Vector2(window.Position.X, window.Position.Y); + position *= window.Scale; + + PendingInputs.Enqueue(new MousePositionAbsoluteInput { Position = position }); + statistic_absolute_events.Value++; + } + else + { + PendingInputs.Enqueue(new MousePositionRelativeInput { Delta = new Vector2(mouse.LastX, mouse.LastY) * sensitivity }); + statistic_relative_events.Value++; + } + + return IntPtr.Zero; + } + } +} diff --git a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs index 0494d54814..cecf086806 100644 --- a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs @@ -115,7 +115,7 @@ protected override void HandleEventFromFilter(SDL_Event e) } /// - /// The last mouse position as reported by . + /// The last mouse position as reported by . /// internal Vector2? LastMousePosition { private get; set; } diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs b/osu.Framework/Platform/Windows/SDL3WindowsMouseHandler.cs similarity index 84% rename from osu.Framework/Platform/Windows/WindowsMouseHandler.cs rename to osu.Framework/Platform/Windows/SDL3WindowsMouseHandler.cs index b74535b787..65d83d3db8 100644 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsMouseHandler.cs @@ -12,13 +12,13 @@ namespace osu.Framework.Platform.Windows /// This is done to better handle quirks of some devices. /// [SupportedOSPlatform("windows")] - internal class WindowsMouseHandler : MouseHandler + internal class SDL3WindowsMouseHandler : MouseHandler { - private WindowsWindow window = null!; + private SDL3WindowsWindow window = null!; public override bool Initialize(GameHost host) { - if (!(host.Window is WindowsWindow desktopWindow)) + if (!(host.Window is SDL3WindowsWindow desktopWindow)) return false; window = desktopWindow; diff --git a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index 36b96269ec..3a30ef7020 100644 --- a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -105,7 +105,7 @@ protected override void HandleEventFromFilter(SDL_Event evt) } /// - /// The last mouse position as reported by . + /// The last mouse position as reported by . /// internal Vector2? LastMousePosition { private get; set; } diff --git a/osu.Framework/Platform/Windows/WindowsGameHost.cs b/osu.Framework/Platform/Windows/WindowsGameHost.cs index f27fd1c4d7..d3b7aea1be 100644 --- a/osu.Framework/Platform/Windows/WindowsGameHost.cs +++ b/osu.Framework/Platform/Windows/WindowsGameHost.cs @@ -71,7 +71,12 @@ protected override IEnumerable CreateAvailableInputHandlers() // for windows platforms we want to override the relative mouse event handling behaviour. return base.CreateAvailableInputHandlers() .Where(t => !(t is MouseHandler)) - .Concat(new InputHandler[] { new WindowsMouseHandler() }); + .Concat(new InputHandler[] + { + FrameworkEnvironment.UseSDL3 + ? new SDL3WindowsMouseHandler() + : new SDL2WindowsMouseHandler() + }); } protected override IRenderer CreateGLRenderer() => new WindowsGLRenderer(this); From 58c99580af09a2148bdbead7942210a4e6589daa Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 19:11:18 +0900 Subject: [PATCH 048/140] Adjust condition --- osu.Framework/Platform/SDLGameHost.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Platform/SDLGameHost.cs b/osu.Framework/Platform/SDLGameHost.cs index db5b000f60..41e237db9b 100644 --- a/osu.Framework/Platform/SDLGameHost.cs +++ b/osu.Framework/Platform/SDLGameHost.cs @@ -33,9 +33,10 @@ protected override TextInputSource CreateTextInput() return base.CreateTextInput(); } - protected override Clipboard CreateClipboard() => Window is SDL3Window - ? new SDL3Clipboard(PngFormat.Instance) // PNG works well on linux - : new SDL2Clipboard(); + protected override Clipboard CreateClipboard() + => FrameworkEnvironment.UseSDL3 + ? new SDL3Clipboard(PngFormat.Instance) // PNG works well on linux + : new SDL2Clipboard(); protected override IEnumerable CreateAvailableInputHandlers() => new InputHandler[] From 7bfc9fe53202873c9d4ebcde5530a8dd23222942 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 19:17:21 +0900 Subject: [PATCH 049/140] Make tests work use ISDLWindow interface --- .../Visual/Platform/TestSceneBorderless.cs | 5 ++--- .../Visual/Platform/TestSceneFullscreen.cs | 3 +-- osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs | 5 ++--- .../Visual/Platform/WindowDisplaysPreview.cs | 5 ++--- osu.Framework/Platform/ISDLWindow.cs | 3 +++ osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs | 2 +- osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs | 8 +++++--- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs b/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs index 63c6a53ebc..4db6b85479 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneBorderless.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; namespace osu.Framework.Tests.Visual.Platform { @@ -25,7 +24,7 @@ public partial class TestSceneBorderless : FrameworkTestScene private readonly SpriteText currentWindowMode = new SpriteText(); private readonly SpriteText currentDisplay = new SpriteText(); - private SDL3Window? window; + private ISDLWindow? window; private readonly Bindable windowMode = new Bindable(); public TestSceneBorderless() @@ -58,7 +57,7 @@ public TestSceneBorderless() [BackgroundDependencyLoader] private void load(FrameworkConfigManager config, GameHost host) { - window = host.Window as SDL3Window; + window = host.Window as ISDLWindow; config.BindWith(FrameworkSetting.WindowMode, windowMode); windowMode.BindValueChanged(mode => currentWindowMode.Text = $"Window Mode: {mode.NewValue}", true); diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs b/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs index f90c3f30f6..4d4d91857c 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneFullscreen.cs @@ -19,7 +19,6 @@ using osu.Framework.Input; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osuTK; using WindowState = osu.Framework.Platform.WindowState; @@ -130,7 +129,7 @@ public void TestScreenModeSwitch() if (window.SupportedWindowModes.Contains(WindowMode.Fullscreen)) { AddStep("change to fullscreen", () => windowMode.Value = WindowMode.Fullscreen); - AddAssert("window position updated", () => ((SDL3Window)window).Position, () => Is.EqualTo(window.CurrentDisplayBindable.Value.Bounds.Location)); + AddAssert("window position updated", () => ((ISDLWindow)window).Position, () => Is.EqualTo(window.CurrentDisplayBindable.Value.Bounds.Location)); testResolution(1920, 1080); testResolution(1280, 960); testResolution(9999, 9999); diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs b/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs index b192f832e6..77248a6e16 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneWindowed.cs @@ -12,7 +12,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osuTK; using WindowState = osu.Framework.Platform.WindowState; @@ -32,12 +31,12 @@ public partial class TestSceneWindowed : FrameworkTestScene [Resolved] private FrameworkConfigManager config { get; set; } - private SDL3Window sdlWindow; + private ISDLWindow sdlWindow; [BackgroundDependencyLoader] private void load() { - sdlWindow = (SDL3Window)host.Window; + sdlWindow = (ISDLWindow)host.Window; Children = new Drawable[] { new FillFlowContainer diff --git a/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs b/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs index 86a772258c..03ee8503af 100644 --- a/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs +++ b/osu.Framework.Tests/Visual/Platform/WindowDisplaysPreview.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; -using osu.Framework.Platform.SDL3; using osuTK; using osuTK.Graphics; @@ -37,7 +36,7 @@ public partial class WindowDisplaysPreview : Container private static readonly Color4 window_fill = new Color4(95, 113, 197, 255); private static readonly Color4 window_stroke = new Color4(36, 59, 166, 255); - private SDL3Window? window; + private ISDLWindow? window; private readonly Bindable windowMode = new Bindable(); private readonly Bindable currentDisplay = new Bindable(); @@ -91,7 +90,7 @@ public WindowDisplaysPreview() [BackgroundDependencyLoader] private void load(FrameworkConfigManager config, GameHost host) { - window = host.Window as SDL3Window; + window = host.Window as ISDLWindow; config.BindWith(FrameworkSetting.WindowMode, windowMode); if (window != null) diff --git a/osu.Framework/Platform/ISDLWindow.cs b/osu.Framework/Platform/ISDLWindow.cs index ab6f9b4087..66f627e28e 100644 --- a/osu.Framework/Platform/ISDLWindow.cs +++ b/osu.Framework/Platform/ISDLWindow.cs @@ -33,6 +33,9 @@ internal interface ISDLWindow : IWindow Point Position { get; } Size Size { get; } + float Scale { get; } + bool Resizable { get; set; } + bool MouseAutoCapture { set; } bool RelativeMouseMode { get; set; } bool CapsLockPressed { get; } diff --git a/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs b/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs index bdac81f47a..2584309b5f 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window_Windowing.cs @@ -277,7 +277,7 @@ public WindowState WindowState /// public Size ClientSize => new Size((int)(Size.Width * Scale), (int)(Size.Height * Scale)); - public float Scale = 1; + public float Scale { get; private set; } = 1; #region Displays (mostly self-contained) diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs index fc66065215..e380f23000 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs @@ -284,7 +284,7 @@ public WindowState WindowState /// public Size ClientSize => new Size((int)(Size.Width * Scale), (int)(Size.Height * Scale)); - public float Scale = 1; + public float Scale { get; private set; } = 1; #region Displays (mostly self-contained) @@ -876,14 +876,16 @@ private static unsafe SDL_DisplayMode getClosestDisplayMode(SDL_Window* windowHa if (mode != null) return *mode; else - Logger.Log($"Unable to get preferred display mode (try #1/2). Target display: {display.Index}, mode: {size.Width}x{size.Height}@{requestedMode.RefreshRate}. SDL error: {SDL3Extensions.GetAndClearError()}"); + Logger.Log( + $"Unable to get preferred display mode (try #1/2). Target display: {display.Index}, mode: {size.Width}x{size.Height}@{requestedMode.RefreshRate}. SDL error: {SDL3Extensions.GetAndClearError()}"); // fallback to current display's native bounds mode = SDL_GetClosestFullscreenDisplayMode(displayID, display.Bounds.Width, display.Bounds.Height, 0f, SDL_bool.SDL_TRUE); if (mode != null) return *mode; else - Logger.Log($"Unable to get preferred display mode (try #2/2). Target display: {display.Index}, mode: {display.Bounds.Width}x{display.Bounds.Height}@default. SDL error: {SDL3Extensions.GetAndClearError()}"); + Logger.Log( + $"Unable to get preferred display mode (try #2/2). Target display: {display.Index}, mode: {display.Bounds.Width}x{display.Bounds.Height}@default. SDL error: {SDL3Extensions.GetAndClearError()}"); // try the display's native display mode. mode = SDL_GetDesktopDisplayMode(displayID); From 1b7438fe114c082a4b10f67bd40c3684d07079ba Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 19:24:17 +0900 Subject: [PATCH 050/140] Fix iOS/Android projects, always use SDL3 on them --- osu.Framework.Android/AndroidGameHost.cs | 2 +- osu.Framework.Android/AndroidGameWindow.cs | 1 + osu.Framework.iOS/IOSGameHost.cs | 2 +- osu.Framework.iOS/IOSWindow.cs | 1 + osu.Framework/FrameworkEnvironment.cs | 2 +- 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Framework.Android/AndroidGameHost.cs b/osu.Framework.Android/AndroidGameHost.cs index 6377b1c121..9754a275e0 100644 --- a/osu.Framework.Android/AndroidGameHost.cs +++ b/osu.Framework.Android/AndroidGameHost.cs @@ -21,7 +21,7 @@ namespace osu.Framework.Android { - public class AndroidGameHost : SDL3GameHost + public class AndroidGameHost : SDLGameHost { private readonly AndroidGameActivity activity; diff --git a/osu.Framework.Android/AndroidGameWindow.cs b/osu.Framework.Android/AndroidGameWindow.cs index 3b11e588b6..457d5e1ebe 100644 --- a/osu.Framework.Android/AndroidGameWindow.cs +++ b/osu.Framework.Android/AndroidGameWindow.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; namespace osu.Framework.Android { diff --git a/osu.Framework.iOS/IOSGameHost.cs b/osu.Framework.iOS/IOSGameHost.cs index 9c2e1256f2..813d6133e6 100644 --- a/osu.Framework.iOS/IOSGameHost.cs +++ b/osu.Framework.iOS/IOSGameHost.cs @@ -21,7 +21,7 @@ namespace osu.Framework.iOS { - public class IOSGameHost : SDL3GameHost + public class IOSGameHost : SDLGameHost { public IOSGameHost() : base(string.Empty) diff --git a/osu.Framework.iOS/IOSWindow.cs b/osu.Framework.iOS/IOSWindow.cs index de8d04e541..674156ec76 100644 --- a/osu.Framework.iOS/IOSWindow.cs +++ b/osu.Framework.iOS/IOSWindow.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Platform; +using osu.Framework.Platform.SDL3; using SDL; using UIKit; diff --git a/osu.Framework/FrameworkEnvironment.cs b/osu.Framework/FrameworkEnvironment.cs index d596ea95d9..5b4b5ee8ee 100644 --- a/osu.Framework/FrameworkEnvironment.cs +++ b/osu.Framework/FrameworkEnvironment.cs @@ -53,7 +53,7 @@ static FrameworkEnvironment() if (DebugUtils.IsDebugBuild) AllowInsecureRequests = parseBool(Environment.GetEnvironmentVariable("OSU_INSECURE_REQUESTS")) ?? false; - UseSDL3 = parseBool(Environment.GetEnvironmentVariable("OSU_SDL3")) ?? false; + UseSDL3 = RuntimeInfo.IsMobile || (parseBool(Environment.GetEnvironmentVariable("OSU_SDL3")) ?? false); } private static bool? parseBool(string? value) From 16f2dd447d67df0e8506135cd7634591a3135767 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 19:29:12 +0900 Subject: [PATCH 051/140] Remove unused classes --- osu.Framework/Input/SDL2WindowTextInput.cs | 70 ------ .../SDL2ReadableKeyCombinationProvider.cs | 230 ------------------ 2 files changed, 300 deletions(-) delete mode 100644 osu.Framework/Input/SDL2WindowTextInput.cs delete mode 100644 osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs diff --git a/osu.Framework/Input/SDL2WindowTextInput.cs b/osu.Framework/Input/SDL2WindowTextInput.cs deleted file mode 100644 index 77e7accfee..0000000000 --- a/osu.Framework/Input/SDL2WindowTextInput.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Primitives; -using osu.Framework.Platform.SDL2; - -namespace osu.Framework.Input -{ - internal class SDL2WindowTextInput : TextInputSource - { - private readonly SDL2Window window; - - public SDL2WindowTextInput(SDL2Window window) - { - this.window = window; - } - - private void handleTextInput(string text) - { - // SDL sends IME results as `SDL_TextInputEvent` which we can't differentiate from regular text input - // so we have to manually keep track and invoke the correct event. - - if (ImeActive) - { - TriggerImeResult(text); - } - else - { - TriggerTextInput(text); - } - } - - private void handleTextEditing(string? text, int selectionStart, int selectionLength) - { - if (text == null) return; - - TriggerImeComposition(text, selectionStart, selectionLength); - } - - protected override void ActivateTextInput(bool allowIme) - { - window.TextInput += handleTextInput; - window.TextEditing += handleTextEditing; - window.StartTextInput(allowIme); - } - - protected override void EnsureTextInputActivated(bool allowIme) - { - window.StartTextInput(allowIme); - } - - protected override void DeactivateTextInput() - { - window.TextInput -= handleTextInput; - window.TextEditing -= handleTextEditing; - window.StopTextInput(); - } - - public override void SetImeRectangle(RectangleF rectangle) - { - window.SetTextInputRect(rectangle); - } - - public override void ResetIme() - { - base.ResetIme(); - window.ResetIme(); - } - } -} diff --git a/osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs b/osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs deleted file mode 100644 index a4f0098278..0000000000 --- a/osu.Framework/Platform/SDL2/SDL2ReadableKeyCombinationProvider.cs +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Globalization; -using osu.Framework.Input; -using osu.Framework.Input.Bindings; -using static SDL2.SDL; - -namespace osu.Framework.Platform.SDL2 -{ - public class SDL2ReadableKeyCombinationProvider : ReadableKeyCombinationProvider - { - protected override string GetReadableKey(InputKey key) - { - var keycode = SDL_GetKeyFromScancode(key.ToScancode()); - - // early return if unknown. probably because key isn't a keyboard key, or doesn't map to an `SDL_Scancode`. - if (keycode == SDL_Keycode.SDLK_UNKNOWN) - return base.GetReadableKey(key); - - string name; - - // overrides for some keys that we want displayed differently from SDL_GetKeyName(). - if (TryGetNameFromKeycode(keycode, out name)) - return name; - - name = SDL_GetKeyName(keycode); - - // fall back if SDL couldn't find a name. - if (string.IsNullOrEmpty(name)) - return base.GetReadableKey(key); - - // true if SDL_GetKeyName() returned a proper key/scancode name. - // see https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/events/SDL_keyboard.c#L1012 - if (((int)keycode & SDLK_SCANCODE_MASK) != 0) - return name; - - // SDL_GetKeyName() returned a unicode character that would be produced if that key was pressed. - // consumers expect an uppercase letter. - // `.ToUpper()` with current culture may be slightly inaccurate if the framework locale - // is different from the locale of the keyboard layout being used, - // but we have no means to detect this anyway, as SDL doesn't provide the name of the layout. - return name.ToUpper(CultureInfo.CurrentCulture); - } - - /// - /// Provides overrides for some keys that we want displayed differently from SDL_GetKeyName(). - /// - /// - /// Should be overriden per-platform to provide platform-specific names for applicable keys. - /// - protected virtual bool TryGetNameFromKeycode(SDL_Keycode keycode, out string name) - { - switch (keycode) - { - case SDL_Keycode.SDLK_RETURN: - name = "Enter"; - return true; - - case SDL_Keycode.SDLK_ESCAPE: - name = "Esc"; - return true; - - case SDL_Keycode.SDLK_BACKSPACE: - name = "Backsp"; - return true; - - case SDL_Keycode.SDLK_TAB: - name = "Tab"; - return true; - - case SDL_Keycode.SDLK_SPACE: - name = "Space"; - return true; - - case SDL_Keycode.SDLK_PLUS: - name = "Plus"; - return true; - - case SDL_Keycode.SDLK_MINUS: - name = "Minus"; - return true; - - case SDL_Keycode.SDLK_DELETE: - name = "Del"; - return true; - - case SDL_Keycode.SDLK_CAPSLOCK: - name = "Caps"; - return true; - - case SDL_Keycode.SDLK_INSERT: - name = "Ins"; - return true; - - case SDL_Keycode.SDLK_PAGEUP: - name = "PgUp"; - return true; - - case SDL_Keycode.SDLK_PAGEDOWN: - name = "PgDn"; - return true; - - case SDL_Keycode.SDLK_NUMLOCKCLEAR: - name = "NumLock"; - return true; - - case SDL_Keycode.SDLK_KP_DIVIDE: - name = "NumpadDivide"; - return true; - - case SDL_Keycode.SDLK_KP_MULTIPLY: - name = "NumpadMultiply"; - return true; - - case SDL_Keycode.SDLK_KP_MINUS: - name = "NumpadMinus"; - return true; - - case SDL_Keycode.SDLK_KP_PLUS: - name = "NumpadPlus"; - return true; - - case SDL_Keycode.SDLK_KP_ENTER: - name = "NumpadEnter"; - return true; - - case SDL_Keycode.SDLK_KP_PERIOD: - name = "NumpadDecimal"; - return true; - - case SDL_Keycode.SDLK_KP_0: - name = "Numpad0"; - return true; - - case SDL_Keycode.SDLK_KP_1: - name = "Numpad1"; - return true; - - case SDL_Keycode.SDLK_KP_2: - name = "Numpad2"; - return true; - - case SDL_Keycode.SDLK_KP_3: - name = "Numpad3"; - return true; - - case SDL_Keycode.SDLK_KP_4: - name = "Numpad4"; - return true; - - case SDL_Keycode.SDLK_KP_5: - name = "Numpad5"; - return true; - - case SDL_Keycode.SDLK_KP_6: - name = "Numpad6"; - return true; - - case SDL_Keycode.SDLK_KP_7: - name = "Numpad7"; - return true; - - case SDL_Keycode.SDLK_KP_8: - name = "Numpad8"; - return true; - - case SDL_Keycode.SDLK_KP_9: - name = "Numpad9"; - return true; - - case SDL_Keycode.SDLK_LCTRL: - name = "LCtrl"; - return true; - - case SDL_Keycode.SDLK_LSHIFT: - name = "LShift"; - return true; - - case SDL_Keycode.SDLK_LALT: - name = "LAlt"; - return true; - - case SDL_Keycode.SDLK_RCTRL: - name = "RCtrl"; - return true; - - case SDL_Keycode.SDLK_RSHIFT: - name = "RShift"; - return true; - - case SDL_Keycode.SDLK_RALT: - name = "RAlt"; - return true; - - case SDL_Keycode.SDLK_VOLUMEUP: - name = "Vol. Up"; - return true; - - case SDL_Keycode.SDLK_VOLUMEDOWN: - name = "Vol. Down"; - return true; - - case SDL_Keycode.SDLK_AUDIONEXT: - name = "Media Next"; - return true; - - case SDL_Keycode.SDLK_AUDIOPREV: - name = "Media Previous"; - return true; - - case SDL_Keycode.SDLK_AUDIOSTOP: - name = "Media Stop"; - return true; - - case SDL_Keycode.SDLK_AUDIOPLAY: - name = "Media Play"; - return true; - - case SDL_Keycode.SDLK_AUDIOMUTE: - name = "Mute"; - return true; - - default: - name = string.Empty; - return false; - } - } - } -} From b937b04418290c4bf3d3f079bf9d099d0fafb071 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 19:46:07 +0900 Subject: [PATCH 052/140] Merge SDL2/3 windows mouse handler implementations --- .../Platform/Windows/IWindowsWindow.cs | 15 +++++++ .../Platform/Windows/SDL2WindowsWindow.cs | 7 +-- .../Windows/SDL3WindowsMouseHandler.cs | 34 -------------- .../Platform/Windows/SDL3WindowsWindow.cs | 7 +-- .../Platform/Windows/WindowsGameHost.cs | 7 +-- ...MouseHandler.cs => WindowsMouseHandler.cs} | 44 ++++++++++++------- 6 files changed, 48 insertions(+), 66 deletions(-) create mode 100644 osu.Framework/Platform/Windows/IWindowsWindow.cs delete mode 100644 osu.Framework/Platform/Windows/SDL3WindowsMouseHandler.cs rename osu.Framework/Platform/Windows/{SDL2WindowsMouseHandler.cs => WindowsMouseHandler.cs} (82%) diff --git a/osu.Framework/Platform/Windows/IWindowsWindow.cs b/osu.Framework/Platform/Windows/IWindowsWindow.cs new file mode 100644 index 0000000000..19b0431d41 --- /dev/null +++ b/osu.Framework/Platform/Windows/IWindowsWindow.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osuTK; + +namespace osu.Framework.Platform.Windows +{ + internal interface IWindowsWindow : ISDLWindow + { + /// + /// The last mouse position as reported by . + /// + Vector2? LastMousePosition { get; set; } + } +} diff --git a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs index cecf086806..4284cbf0e1 100644 --- a/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL2WindowsWindow.cs @@ -16,7 +16,7 @@ namespace osu.Framework.Platform.Windows { [SupportedOSPlatform("windows")] - internal class SDL2WindowsWindow : SDL2DesktopWindow + internal class SDL2WindowsWindow : SDL2DesktopWindow, IWindowsWindow { private const int seticon_message = 0x0080; private const int icon_big = 1; @@ -114,10 +114,7 @@ protected override void HandleEventFromFilter(SDL_Event e) base.HandleEventFromFilter(e); } - /// - /// The last mouse position as reported by . - /// - internal Vector2? LastMousePosition { private get; set; } + public Vector2? LastMousePosition { get; set; } /// /// If required, warps the OS cursor to match the framework cursor position. diff --git a/osu.Framework/Platform/Windows/SDL3WindowsMouseHandler.cs b/osu.Framework/Platform/Windows/SDL3WindowsMouseHandler.cs deleted file mode 100644 index 65d83d3db8..0000000000 --- a/osu.Framework/Platform/Windows/SDL3WindowsMouseHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Runtime.Versioning; -using osu.Framework.Input.Handlers.Mouse; -using osuTK; - -namespace osu.Framework.Platform.Windows -{ - /// - /// A windows specific mouse input handler which overrides the SDL3 implementation of raw input. - /// This is done to better handle quirks of some devices. - /// - [SupportedOSPlatform("windows")] - internal class SDL3WindowsMouseHandler : MouseHandler - { - private SDL3WindowsWindow window = null!; - - public override bool Initialize(GameHost host) - { - if (!(host.Window is SDL3WindowsWindow desktopWindow)) - return false; - - window = desktopWindow; - return base.Initialize(host); - } - - public override void FeedbackMousePositionChange(Vector2 position, bool isSelfFeedback) - { - window.LastMousePosition = position; - base.FeedbackMousePositionChange(position, isSelfFeedback); - } - } -} diff --git a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs index 3a30ef7020..698c3a4192 100644 --- a/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs +++ b/osu.Framework/Platform/Windows/SDL3WindowsWindow.cs @@ -20,7 +20,7 @@ namespace osu.Framework.Platform.Windows { [SupportedOSPlatform("windows")] - internal class SDL3WindowsWindow : SDL3DesktopWindow + internal class SDL3WindowsWindow : SDL3DesktopWindow, IWindowsWindow { private const int seticon_message = 0x0080; private const int icon_big = 1; @@ -104,10 +104,7 @@ protected override void HandleEventFromFilter(SDL_Event evt) base.HandleEventFromFilter(evt); } - /// - /// The last mouse position as reported by . - /// - internal Vector2? LastMousePosition { private get; set; } + public Vector2? LastMousePosition { get; set; } /// /// If required, warps the OS cursor to match the framework cursor position. diff --git a/osu.Framework/Platform/Windows/WindowsGameHost.cs b/osu.Framework/Platform/Windows/WindowsGameHost.cs index d3b7aea1be..f27fd1c4d7 100644 --- a/osu.Framework/Platform/Windows/WindowsGameHost.cs +++ b/osu.Framework/Platform/Windows/WindowsGameHost.cs @@ -71,12 +71,7 @@ protected override IEnumerable CreateAvailableInputHandlers() // for windows platforms we want to override the relative mouse event handling behaviour. return base.CreateAvailableInputHandlers() .Where(t => !(t is MouseHandler)) - .Concat(new InputHandler[] - { - FrameworkEnvironment.UseSDL3 - ? new SDL3WindowsMouseHandler() - : new SDL2WindowsMouseHandler() - }); + .Concat(new InputHandler[] { new WindowsMouseHandler() }); } protected override IRenderer CreateGLRenderer() => new WindowsGLRenderer(this); diff --git a/osu.Framework/Platform/Windows/SDL2WindowsMouseHandler.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs similarity index 82% rename from osu.Framework/Platform/Windows/SDL2WindowsMouseHandler.cs rename to osu.Framework/Platform/Windows/WindowsMouseHandler.cs index 281d4fc9f0..6f227258d1 100644 --- a/osu.Framework/Platform/Windows/SDL2WindowsMouseHandler.cs +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs @@ -10,46 +10,54 @@ using osu.Framework.Platform.Windows.Native; using osu.Framework.Statistics; using osuTK; -using static SDL2.SDL; namespace osu.Framework.Platform.Windows { /// - /// A windows specific mouse input handler which overrides the SDL2 implementation of raw input. + /// A windows specific mouse input handler which overrides the SDL3 implementation of raw input. /// This is done to better handle quirks of some devices. /// [SupportedOSPlatform("windows")] - internal unsafe class SDL2WindowsMouseHandler : MouseHandler + internal class WindowsMouseHandler : MouseHandler { - private static readonly GlobalStatistic statistic_relative_events = GlobalStatistics.Get(StatisticGroupFor(), "Relative events"); - private static readonly GlobalStatistic statistic_absolute_events = GlobalStatistics.Get(StatisticGroupFor(), "Absolute events"); - private static readonly GlobalStatistic statistic_dropped_touch_inputs = GlobalStatistics.Get(StatisticGroupFor(), "Dropped native touch inputs"); + private static readonly GlobalStatistic statistic_relative_events = GlobalStatistics.Get(StatisticGroupFor(), "Relative events"); + private static readonly GlobalStatistic statistic_absolute_events = GlobalStatistics.Get(StatisticGroupFor(), "Absolute events"); + private static readonly GlobalStatistic statistic_dropped_touch_inputs = GlobalStatistics.Get(StatisticGroupFor(), "Dropped native touch inputs"); private static readonly GlobalStatistic statistic_inputs_with_extra_information = - GlobalStatistics.Get(StatisticGroupFor(), "Native inputs with ExtraInformation"); + GlobalStatistics.Get(StatisticGroupFor(), "Native inputs with ExtraInformation"); private const int raw_input_coordinate_space = 65535; - private SDL_WindowsMessageHook callback = null!; - private SDL2WindowsWindow window = null!; + private global::SDL2.SDL.SDL_WindowsMessageHook callback = null!; + private IWindowsWindow window = null!; public override bool IsActive => Enabled.Value; public override bool Initialize(GameHost host) { - if (!(host.Window is SDL2WindowsWindow desktopWindow)) + if (host.Window is not IWindowsWindow windowsWindow) return false; - window = desktopWindow; + window = windowsWindow; + bindHandler(host); + + return base.Initialize(host); + } + + private void bindHandler(GameHost host) + { + // SDL3 does not (yet?) support binding to WndProc. + if (window is SDL3WindowsWindow) + return; + // ReSharper disable once ConvertClosureToMethodGroup callback = (ptr, wnd, u, param, l) => onWndProc(ptr, wnd, u, param, l); Enabled.BindValueChanged(enabled => { - host.InputThread.Scheduler.Add(() => SDL_SetWindowsMessageHook(enabled.NewValue ? callback : null, IntPtr.Zero)); + host.InputThread.Scheduler.Add(() => global::SDL2.SDL.SDL_SetWindowsMessageHook(enabled.NewValue ? callback : null, IntPtr.Zero)); }, true); - - return base.Initialize(host); } public override void FeedbackMousePositionChange(Vector2 position, bool isSelfFeedback) @@ -60,10 +68,14 @@ public override void FeedbackMousePositionChange(Vector2 position, bool isSelfFe protected override void HandleMouseMoveRelative(Vector2 delta) { - // handled via custom logic below. + // SDL2 reports relative movement via the WndProc handler. + if (window is SDL2WindowsWindow) + return; + + base.HandleMouseMoveRelative(delta); } - private IntPtr onWndProc(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) + private unsafe IntPtr onWndProc(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) { if (!Enabled.Value) return IntPtr.Zero; From d46a1322c2ebb76b034cf9526047f148c7a026bb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 20:04:30 +0900 Subject: [PATCH 053/140] Cleanups --- osu.Framework/Platform/SDL3/SDL3Clipboard.cs | 19 +++--- .../Platform/SDL3/SDL3ControllerBindings.cs | 2 +- osu.Framework/Platform/SDL3/SDL3Extensions.cs | 11 ++-- .../Platform/SDL3/SDL3GraphicsSurface.cs | 61 ++++++++++--------- .../SDL3ReadableKeyCombinationProvider.cs | 7 ++- .../Platform/SDL3/SDL3Window_Windowing.cs | 24 ++++---- 6 files changed, 64 insertions(+), 60 deletions(-) diff --git a/osu.Framework/Platform/SDL3/SDL3Clipboard.cs b/osu.Framework/Platform/SDL3/SDL3Clipboard.cs index b3d1e42733..ac56a6ecfe 100644 --- a/osu.Framework/Platform/SDL3/SDL3Clipboard.cs +++ b/osu.Framework/Platform/SDL3/SDL3Clipboard.cs @@ -15,6 +15,7 @@ using SDL; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; +using static SDL.SDL3; namespace osu.Framework.Platform.SDL3 { @@ -40,9 +41,9 @@ public SDL3Clipboard(IImageFormat imageFormat) // SDL cannot differentiate between string.Empty and no text (eg. empty clipboard or an image) // doesn't matter as text editors don't really allow copying empty strings. // assume that empty text means no text. - public override string? GetText() => SDL.SDL3.SDL_HasClipboardText() == SDL_bool.SDL_TRUE ? SDL.SDL3.SDL_GetClipboardText() : null; + public override string? GetText() => SDL_HasClipboardText() == SDL_bool.SDL_TRUE ? SDL_GetClipboardText() : null; - public override void SetText(string text) => SDL.SDL3.SDL_SetClipboardText(text); + public override void SetText(string text) => SDL_SetClipboardText(text); public override Image? GetImage() { @@ -85,18 +86,18 @@ public override bool SetImage(Image image) private static unsafe bool tryGetData(string mimeType, SpanDecoder decoder, out T? data) { - if (SDL.SDL3.SDL_HasClipboardData(mimeType) == SDL_bool.SDL_FALSE) + if (SDL_HasClipboardData(mimeType) == SDL_bool.SDL_FALSE) { data = default; return false; } UIntPtr nativeSize; - IntPtr pointer = SDL.SDL3.SDL_GetClipboardData(mimeType, &nativeSize); + IntPtr pointer = SDL_GetClipboardData(mimeType, &nativeSize); if (pointer == IntPtr.Zero) { - Logger.Log($"Failed to get SDL clipboard data for {mimeType}. SDL error: {SDL.SDL3.SDL_GetError()}"); + Logger.Log($"Failed to get SDL clipboard data for {mimeType}. SDL error: {SDL_GetError()}"); data = default; return false; } @@ -115,7 +116,7 @@ private static unsafe bool tryGetData(string mimeType, SpanDecoder decoder } finally { - SDL.SDL3.SDL_free(pointer); + SDL_free(pointer); } } @@ -127,12 +128,12 @@ private static unsafe bool trySetData(string mimeType, Func // TODO: support multiple mime types in a single callback fixed (byte* ptr = Encoding.UTF8.GetBytes(mimeType + '\0')) { - int ret = SDL.SDL3.SDL_SetClipboardData(&dataCallback, &cleanupCallback, objectHandle.Handle, &ptr, 1); + int ret = SDL_SetClipboardData(&dataCallback, &cleanupCallback, objectHandle.Handle, &ptr, 1); if (ret < 0) { objectHandle.Dispose(); - Logger.Log($"Failed to set clipboard data callback. SDL error: {SDL.SDL3.SDL_GetError()}"); + Logger.Log($"Failed to set clipboard data callback. SDL error: {SDL_GetError()}"); } return ret == 0; @@ -144,7 +145,7 @@ private static unsafe IntPtr dataCallback(IntPtr userdata, byte* mimeType, UIntP { using var objectHandle = new ObjectHandle(userdata); - if (!objectHandle.GetTarget(out var context) || context.MimeType != SDL.SDL3.PtrToStringUTF8(mimeType)) + if (!objectHandle.GetTarget(out var context) || context.MimeType != PtrToStringUTF8(mimeType)) { *length = 0; return IntPtr.Zero; diff --git a/osu.Framework/Platform/SDL3/SDL3ControllerBindings.cs b/osu.Framework/Platform/SDL3/SDL3ControllerBindings.cs index cf7c1a237d..5d525218ff 100644 --- a/osu.Framework/Platform/SDL3/SDL3ControllerBindings.cs +++ b/osu.Framework/Platform/SDL3/SDL3ControllerBindings.cs @@ -40,7 +40,7 @@ public void PopulateBindings() return; } - using var bindings = SDL.SDL3.SDL_GetGamepadBindings(GamepadHandle); + using var bindings = SDL_GetGamepadBindings(GamepadHandle); if (bindings == null) { diff --git a/osu.Framework/Platform/SDL3/SDL3Extensions.cs b/osu.Framework/Platform/SDL3/SDL3Extensions.cs index de9ed74258..391e2c6ab5 100644 --- a/osu.Framework/Platform/SDL3/SDL3Extensions.cs +++ b/osu.Framework/Platform/SDL3/SDL3Extensions.cs @@ -9,6 +9,7 @@ using osu.Framework.Input.Bindings; using osuTK.Input; using SDL; +using static SDL.SDL3; namespace osu.Framework.Platform.SDL3 { @@ -1016,8 +1017,8 @@ public static unsafe DisplayMode ToDisplayMode(this SDL_DisplayMode mode, int di { int bpp; uint unused; - SDL.SDL3.SDL_GetMasksForPixelFormatEnum(mode.format, &bpp, &unused, &unused, &unused, &unused); - return new DisplayMode(SDL.SDL3.SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex); + SDL_GetMasksForPixelFormatEnum(mode.format, &bpp, &unused, &unused, &unused, &unused); + return new DisplayMode(SDL_GetPixelFormatName(mode.format), new Size(mode.w, mode.h), bpp, mode.refresh_rate, displayIndex); } public static string ReadableName(this SDL_LogCategory category) @@ -1096,8 +1097,8 @@ public static string ReadableName(this SDL_LogPriority priority) /// public static string? GetAndClearError() { - string? error = SDL.SDL3.SDL_GetError(); - SDL.SDL3.SDL_ClearError(); + string? error = SDL_GetError(); + SDL_ClearError(); return error; } @@ -1109,7 +1110,7 @@ public static string ReadableName(this SDL_LogPriority priority) /// public static bool TryGetTouchName(this SDL_TouchFingerEvent e, [NotNullWhen(true)] out string? name) { - name = SDL.SDL3.SDL_GetTouchDeviceName(e.touchID); + name = SDL_GetTouchDeviceName(e.touchID); return name != null; } } diff --git a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs index 8ebf4fc0de..bf0030c0b1 100644 --- a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs +++ b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs @@ -11,6 +11,7 @@ using osuTK.Graphics; using osuTK.Graphics.ES30; using SDL; +using static SDL.SDL3; namespace osu.Framework.Platform.SDL3 { @@ -32,12 +33,12 @@ public SDL3GraphicsSurface(SDL3Window window, GraphicsSurfaceType surfaceType) switch (surfaceType) { case GraphicsSurfaceType.OpenGL: - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_RED_SIZE, 8); - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_GREEN_SIZE, 8); - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_BLUE_SIZE, 8); - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ACCUM_ALPHA_SIZE, 0); - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DEPTH_SIZE, 16); - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STENCIL_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ACCUM_ALPHA_SIZE, 0); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DEPTH_SIZE, 16); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STENCIL_SIZE, 8); break; case GraphicsSurfaceType.Vulkan: @@ -59,7 +60,7 @@ public void Initialise() public Size GetDrawableSize() { int width, height; - SDL.SDL3.SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height); + SDL_GetWindowSizeInPixels(window.SDLWindowHandle, &width, &height); return new Size(width, height); } @@ -69,27 +70,27 @@ private void initialiseOpenGL() { if (RuntimeInfo.IsMobile) { - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_ES); // Minimum OpenGL version for ES profile: - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 0); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 0); } else { - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_CORE); // Minimum OpenGL version for core profile: - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL.SDL3.SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 2); } - context = SDL.SDL3.SDL_GL_CreateContext(window.SDLWindowHandle); + context = SDL_GL_CreateContext(window.SDLWindowHandle); if (context == IntPtr.Zero) - throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL.SDL3.SDL_GetError()})"); + throw new InvalidOperationException($"Failed to create an SDL3 GL context ({SDL_GetError()})"); - SDL.SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, context); + SDL_GL_MakeCurrent(window.SDLWindowHandle, context); loadBindings(); } @@ -135,15 +136,15 @@ private void loadEntryPoints(GraphicsBindingsBase bindings) private IntPtr getProcAddress(string symbol) { const SDL_LogCategory error_category = SDL_LogCategory.SDL_LOG_CATEGORY_ERROR; - SDL_LogPriority oldPriority = SDL.SDL3.SDL_LogGetPriority(error_category); + SDL_LogPriority oldPriority = SDL_LogGetPriority(error_category); // Prevent logging calls to SDL_GL_GetProcAddress() that fail on systems which don't have the requested symbol (typically macOS). - SDL.SDL3.SDL_LogSetPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO); + SDL_LogSetPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO); - IntPtr ret = SDL.SDL3.SDL_GL_GetProcAddress(symbol); + IntPtr ret = SDL_GL_GetProcAddress(symbol); // Reset the logging behaviour. - SDL.SDL3.SDL_LogSetPriority(error_category, oldPriority); + SDL_LogSetPriority(error_category, oldPriority); return ret; } @@ -180,34 +181,34 @@ bool IOpenGLGraphicsSurface.VerticalSync return verticalSync.Value; int interval; - SDL.SDL3.SDL_GL_GetSwapInterval(&interval); + SDL_GL_GetSwapInterval(&interval); return (verticalSync = interval != 0).Value; } set { if (RuntimeInfo.IsDesktop) { - SDL.SDL3.SDL_GL_SetSwapInterval(value ? 1 : 0); + SDL_GL_SetSwapInterval(value ? 1 : 0); verticalSync = value; } } } IntPtr IOpenGLGraphicsSurface.WindowContext => context; - IntPtr IOpenGLGraphicsSurface.CurrentContext => SDL.SDL3.SDL_GL_GetCurrentContext(); + IntPtr IOpenGLGraphicsSurface.CurrentContext => SDL_GL_GetCurrentContext(); - void IOpenGLGraphicsSurface.SwapBuffers() => SDL.SDL3.SDL_GL_SwapWindow(window.SDLWindowHandle); - void IOpenGLGraphicsSurface.CreateContext() => SDL.SDL3.SDL_GL_CreateContext(window.SDLWindowHandle); - void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL.SDL3.SDL_GL_DeleteContext(context); - void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL.SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, context); - void IOpenGLGraphicsSurface.ClearCurrent() => SDL.SDL3.SDL_GL_MakeCurrent(window.SDLWindowHandle, IntPtr.Zero); + void IOpenGLGraphicsSurface.SwapBuffers() => SDL_GL_SwapWindow(window.SDLWindowHandle); + void IOpenGLGraphicsSurface.CreateContext() => SDL_GL_CreateContext(window.SDLWindowHandle); + void IOpenGLGraphicsSurface.DeleteContext(IntPtr context) => SDL_GL_DeleteContext(context); + void IOpenGLGraphicsSurface.MakeCurrent(IntPtr context) => SDL_GL_MakeCurrent(window.SDLWindowHandle, context); + void IOpenGLGraphicsSurface.ClearCurrent() => SDL_GL_MakeCurrent(window.SDLWindowHandle, IntPtr.Zero); IntPtr IOpenGLGraphicsSurface.GetProcAddress(string symbol) => getProcAddress(symbol); #endregion #region Metal-specific implementation - IntPtr IMetalGraphicsSurface.CreateMetalView() => SDL.SDL3.SDL_Metal_CreateView(window.SDLWindowHandle); + IntPtr IMetalGraphicsSurface.CreateMetalView() => SDL_Metal_CreateView(window.SDLWindowHandle); #endregion @@ -223,7 +224,7 @@ bool IOpenGLGraphicsSurface.VerticalSync #region Android-specific implementation [SupportedOSPlatform("android")] - IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL.SDL3.SDL_AndroidGetJNIEnv(); + IntPtr IAndroidGraphicsSurface.JniEnvHandle => SDL_AndroidGetJNIEnv(); [SupportedOSPlatform("android")] IntPtr IAndroidGraphicsSurface.SurfaceHandle => window.SurfaceHandle; diff --git a/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs b/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs index 92ecc06203..126a73e566 100644 --- a/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs +++ b/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs @@ -5,6 +5,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using SDL; +using static SDL.SDL3; namespace osu.Framework.Platform.SDL3 { @@ -12,7 +13,7 @@ public class SDL3ReadableKeyCombinationProvider : ReadableKeyCombinationProvider { protected override string GetReadableKey(InputKey key) { - var keycode = SDL.SDL3.SDL_GetKeyFromScancode(key.ToScancode()); + var keycode = SDL_GetKeyFromScancode(key.ToScancode()); // early return if unknown. probably because key isn't a keyboard key, or doesn't map to an `SDL_Scancode`. if (keycode == SDL_Keycode.SDLK_UNKNOWN) @@ -24,7 +25,7 @@ protected override string GetReadableKey(InputKey key) if (TryGetNameFromKeycode(keycode, out name)) return name; - name = SDL.SDL3.SDL_GetKeyName(keycode); + name = SDL_GetKeyName(keycode); // fall back if SDL couldn't find a name. if (string.IsNullOrEmpty(name)) @@ -32,7 +33,7 @@ protected override string GetReadableKey(InputKey key) // true if SDL_GetKeyName() returned a proper key/scancode name. // see https://github.com/libsdl-org/SDL/blob/release-2.0.16/src/events/SDL_keyboard.c#L1012 - if (((int)keycode & SDL.SDL3.SDLK_SCANCODE_MASK) != 0) + if (((int)keycode & SDLK_SCANCODE_MASK) != 0) return name; // SDL_GetKeyName() returned a unicode character that would be produced if that key was pressed. diff --git a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs index e380f23000..56aef6b4a4 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window_Windowing.cs @@ -382,7 +382,7 @@ private static unsafe bool tryGetDisplayFromSDL(int displayIndex, SDL_DisplayID displayModes = new DisplayMode[modes.Count]; for (int i = 0; i < modes.Count; i++) - displayModes[i] = SDL3Extensions.ToDisplayMode(modes[i], displayIndex); + displayModes[i] = modes[i].ToDisplayMode(displayIndex); } display = new Display(displayIndex, SDL_GetDisplayName(displayID), new Rectangle(rect.x, rect.y, rect.w, rect.h), displayModes); @@ -583,7 +583,7 @@ private unsafe void updateAndFetchWindowSpecifics() } else { - windowState = SDL3Extensions.ToWindowState(SDL_GetWindowFlags(SDLWindowHandle)); + windowState = SDL_GetWindowFlags(SDLWindowHandle).ToWindowState(); } if (windowState != stateBefore) @@ -875,31 +875,31 @@ private static unsafe SDL_DisplayMode getClosestDisplayMode(SDL_Window* windowHa var mode = SDL_GetClosestFullscreenDisplayMode(displayID, size.Width, size.Height, requestedMode.RefreshRate, SDL_bool.SDL_TRUE); if (mode != null) return *mode; - else - Logger.Log( - $"Unable to get preferred display mode (try #1/2). Target display: {display.Index}, mode: {size.Width}x{size.Height}@{requestedMode.RefreshRate}. SDL error: {SDL3Extensions.GetAndClearError()}"); + + Logger.Log( + $"Unable to get preferred display mode (try #1/2). Target display: {display.Index}, mode: {size.Width}x{size.Height}@{requestedMode.RefreshRate}. SDL error: {SDL3Extensions.GetAndClearError()}"); // fallback to current display's native bounds mode = SDL_GetClosestFullscreenDisplayMode(displayID, display.Bounds.Width, display.Bounds.Height, 0f, SDL_bool.SDL_TRUE); if (mode != null) return *mode; - else - Logger.Log( - $"Unable to get preferred display mode (try #2/2). Target display: {display.Index}, mode: {display.Bounds.Width}x{display.Bounds.Height}@default. SDL error: {SDL3Extensions.GetAndClearError()}"); + + Logger.Log( + $"Unable to get preferred display mode (try #2/2). Target display: {display.Index}, mode: {display.Bounds.Width}x{display.Bounds.Height}@default. SDL error: {SDL3Extensions.GetAndClearError()}"); // try the display's native display mode. mode = SDL_GetDesktopDisplayMode(displayID); if (mode != null) return *mode; - else - Logger.Log($"Failed to get desktop display mode (try #1/1). Target display: {display.Index}. SDL error: {SDL3Extensions.GetAndClearError()}", level: LogLevel.Error); + + Logger.Log($"Failed to get desktop display mode (try #1/1). Target display: {display.Index}. SDL error: {SDL3Extensions.GetAndClearError()}", level: LogLevel.Error); // finally return the current mode if everything else fails. mode = SDL_GetWindowFullscreenMode(windowHandle); if (mode != null) return *mode; - else - Logger.Log($"Failed to get window display mode. SDL error: {SDL3Extensions.GetAndClearError()}", level: LogLevel.Error); + + Logger.Log($"Failed to get window display mode. SDL error: {SDL3Extensions.GetAndClearError()}", level: LogLevel.Error); throw new InvalidOperationException("couldn't retrieve valid display mode"); } From a6d5fb758964a7873c2a3ffd4f7ede75f04a1b2f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 20:30:36 +0900 Subject: [PATCH 054/140] Fix incorrect assertion and possible nullref --- osu.Framework/Graphics/UserInterface/TextBox.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Framework/Graphics/UserInterface/TextBox.cs b/osu.Framework/Graphics/UserInterface/TextBox.cs index 472272b2dc..cc15cd2263 100644 --- a/osu.Framework/Graphics/UserInterface/TextBox.cs +++ b/osu.Framework/Graphics/UserInterface/TextBox.cs @@ -1377,20 +1377,21 @@ private void bindInput([CanBeNull] TextBox previous) private void unbindInput([CanBeNull] TextBox next) { - Debug.Assert(textInput != null); - if (!textInputBound) return; textInputBound = false; - // see the comment above, in `bindInput(bool)`. - if (next?.textInput != textInput) - textInput.Deactivate(); + if (textInput != null) + { + // see the comment above, in `bindInput(bool)`. + if (next?.textInput != textInput) + textInput.Deactivate(); - textInput.OnTextInput -= handleTextInput; - textInput.OnImeComposition -= handleImeComposition; - textInput.OnImeResult -= handleImeResult; + textInput.OnTextInput -= handleTextInput; + textInput.OnImeComposition -= handleImeComposition; + textInput.OnImeResult -= handleImeResult; + } // in case keys are held and we lose focus, we should no longer block key events textInputBlocking = false; From ade299e4be1295b7e597072e1e446090836b6d2e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 22:24:06 +0900 Subject: [PATCH 055/140] Fix readable key combinations not working in SDL2 --- .../SDL3ReadableKeyCombinationProvider.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs b/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs index 126a73e566..9d8d81dabc 100644 --- a/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs +++ b/osu.Framework/Platform/SDL3/SDL3ReadableKeyCombinationProvider.cs @@ -11,9 +11,25 @@ namespace osu.Framework.Platform.SDL3 { public class SDL3ReadableKeyCombinationProvider : ReadableKeyCombinationProvider { + private static SDL_Keycode getKeyFromScancode(SDL_Scancode scancode) + { + if (FrameworkEnvironment.UseSDL3) + return SDL_GetKeyFromScancode(scancode); + + return (SDL_Keycode)global::SDL2.SDL.SDL_GetKeyFromScancode((global::SDL2.SDL.SDL_Scancode)scancode); + } + + private static string? getKeyName(SDL_Keycode keycode) + { + if (FrameworkEnvironment.UseSDL3) + return SDL_GetKeyName(keycode); + + return global::SDL2.SDL.SDL_GetKeyName((global::SDL2.SDL.SDL_Keycode)keycode); + } + protected override string GetReadableKey(InputKey key) { - var keycode = SDL_GetKeyFromScancode(key.ToScancode()); + var keycode = getKeyFromScancode(key.ToScancode()); // early return if unknown. probably because key isn't a keyboard key, or doesn't map to an `SDL_Scancode`. if (keycode == SDL_Keycode.SDLK_UNKNOWN) @@ -25,7 +41,7 @@ protected override string GetReadableKey(InputKey key) if (TryGetNameFromKeycode(keycode, out name)) return name; - name = SDL_GetKeyName(keycode); + name = getKeyName(keycode); // fall back if SDL couldn't find a name. if (string.IsNullOrEmpty(name)) From 6f0702a1200d866470d1413f6efc3576313de7d1 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 22:36:55 +0900 Subject: [PATCH 056/140] Split WindowsMouseHandler into generic and non-generic parts --- .../Platform/Windows/WindowsMouseHandler.cs | 31 ++++++------------- .../Windows/WindowsMouseHandler_SDL2.cs | 26 ++++++++++++++++ .../Windows/WindowsMouseHandler_SDL3.cs | 13 ++++++++ 3 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs create mode 100644 osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs index 6f227258d1..ef2cdcaad3 100644 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs @@ -18,7 +18,7 @@ namespace osu.Framework.Platform.Windows /// This is done to better handle quirks of some devices. /// [SupportedOSPlatform("windows")] - internal class WindowsMouseHandler : MouseHandler + internal partial class WindowsMouseHandler : MouseHandler { private static readonly GlobalStatistic statistic_relative_events = GlobalStatistics.Get(StatisticGroupFor(), "Relative events"); private static readonly GlobalStatistic statistic_absolute_events = GlobalStatistics.Get(StatisticGroupFor(), "Absolute events"); @@ -29,8 +29,8 @@ internal class WindowsMouseHandler : MouseHandler private const int raw_input_coordinate_space = 65535; - private global::SDL2.SDL.SDL_WindowsMessageHook callback = null!; private IWindowsWindow window = null!; + private bool handlerBound; public override bool IsActive => Enabled.Value; @@ -40,26 +40,13 @@ public override bool Initialize(GameHost host) return false; window = windowsWindow; - bindHandler(host); + handlerBound = window is SDL2WindowsWindow + ? bindHandlerSDL2(host) + : bindHandlerSDL3(host); return base.Initialize(host); } - private void bindHandler(GameHost host) - { - // SDL3 does not (yet?) support binding to WndProc. - if (window is SDL3WindowsWindow) - return; - - // ReSharper disable once ConvertClosureToMethodGroup - callback = (ptr, wnd, u, param, l) => onWndProc(ptr, wnd, u, param, l); - - Enabled.BindValueChanged(enabled => - { - host.InputThread.Scheduler.Add(() => global::SDL2.SDL.SDL_SetWindowsMessageHook(enabled.NewValue ? callback : null, IntPtr.Zero)); - }, true); - } - public override void FeedbackMousePositionChange(Vector2 position, bool isSelfFeedback) { window.LastMousePosition = position; @@ -68,14 +55,16 @@ public override void FeedbackMousePositionChange(Vector2 position, bool isSelfFe protected override void HandleMouseMoveRelative(Vector2 delta) { - // SDL2 reports relative movement via the WndProc handler. - if (window is SDL2WindowsWindow) + if (handlerBound) + { + // When bound, relative movement is reported via the WndProc handler below. return; + } base.HandleMouseMoveRelative(delta); } - private unsafe IntPtr onWndProc(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) + private unsafe IntPtr onWndProcSDL2(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) { if (!Enabled.Value) return IntPtr.Zero; diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs new file mode 100644 index 0000000000..093002fcff --- /dev/null +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs @@ -0,0 +1,26 @@ +// 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 static SDL2.SDL; + +namespace osu.Framework.Platform.Windows +{ + internal partial class WindowsMouseHandler + { + private SDL_WindowsMessageHook callback = null!; + + private bool bindHandlerSDL2(GameHost host) + { + // ReSharper disable once ConvertClosureToMethodGroup + callback = (ptr, wnd, u, param, l) => onWndProcSDL2(ptr, wnd, u, param, l); + + Enabled.BindValueChanged(enabled => + { + host.InputThread.Scheduler.Add(() => SDL_SetWindowsMessageHook(enabled.NewValue ? callback : null, IntPtr.Zero)); + }, true); + + return true; + } + } +} diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs new file mode 100644 index 0000000000..0d67175e35 --- /dev/null +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Platform.Windows +{ + internal partial class WindowsMouseHandler + { + private bool bindHandlerSDL3(GameHost host) + { + return false; + } + } +} From 8d5feecb85e9871807d43e19bcfdbc3b92333d54 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 21 May 2024 22:41:41 +0900 Subject: [PATCH 057/140] Rename callback --- osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs index 093002fcff..bd13d7b174 100644 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs @@ -8,16 +8,16 @@ namespace osu.Framework.Platform.Windows { internal partial class WindowsMouseHandler { - private SDL_WindowsMessageHook callback = null!; + private SDL_WindowsMessageHook sdl2Callback = null!; private bool bindHandlerSDL2(GameHost host) { // ReSharper disable once ConvertClosureToMethodGroup - callback = (ptr, wnd, u, param, l) => onWndProcSDL2(ptr, wnd, u, param, l); + sdl2Callback = (ptr, wnd, u, param, l) => onWndProcSDL2(ptr, wnd, u, param, l); Enabled.BindValueChanged(enabled => { - host.InputThread.Scheduler.Add(() => SDL_SetWindowsMessageHook(enabled.NewValue ? callback : null, IntPtr.Zero)); + host.InputThread.Scheduler.Add(() => SDL_SetWindowsMessageHook(enabled.NewValue ? sdl2Callback : null, IntPtr.Zero)); }, true); return true; From d5c09342704cb69323f0f9ec71c011a95401708e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 May 2024 00:22:20 +0900 Subject: [PATCH 058/140] Split entire SDL2 WindowsMouseHandler implementation --- .../Platform/Windows/WindowsMouseHandler.cs | 125 +----------------- .../Windows/WindowsMouseHandler_SDL2.cs | 120 ++++++++++++++++- .../Windows/WindowsMouseHandler_SDL3.cs | 13 -- 3 files changed, 121 insertions(+), 137 deletions(-) delete mode 100644 osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs index ef2cdcaad3..4abe150a6e 100644 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler.cs +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler.cs @@ -1,14 +1,8 @@ // 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.Drawing; using System.Runtime.Versioning; -using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Input.Handlers.Mouse; -using osu.Framework.Input.StateChanges; -using osu.Framework.Platform.Windows.Native; -using osu.Framework.Statistics; using osuTK; namespace osu.Framework.Platform.Windows @@ -20,17 +14,7 @@ namespace osu.Framework.Platform.Windows [SupportedOSPlatform("windows")] internal partial class WindowsMouseHandler : MouseHandler { - private static readonly GlobalStatistic statistic_relative_events = GlobalStatistics.Get(StatisticGroupFor(), "Relative events"); - private static readonly GlobalStatistic statistic_absolute_events = GlobalStatistics.Get(StatisticGroupFor(), "Absolute events"); - private static readonly GlobalStatistic statistic_dropped_touch_inputs = GlobalStatistics.Get(StatisticGroupFor(), "Dropped native touch inputs"); - - private static readonly GlobalStatistic statistic_inputs_with_extra_information = - GlobalStatistics.Get(StatisticGroupFor(), "Native inputs with ExtraInformation"); - - private const int raw_input_coordinate_space = 65535; - private IWindowsWindow window = null!; - private bool handlerBound; public override bool IsActive => Enabled.Value; @@ -40,9 +24,9 @@ public override bool Initialize(GameHost host) return false; window = windowsWindow; - handlerBound = window is SDL2WindowsWindow - ? bindHandlerSDL2(host) - : bindHandlerSDL3(host); + + if (window is SDL2WindowsWindow) + initialiseSDL2(host); return base.Initialize(host); } @@ -52,108 +36,5 @@ public override void FeedbackMousePositionChange(Vector2 position, bool isSelfFe window.LastMousePosition = position; base.FeedbackMousePositionChange(position, isSelfFeedback); } - - protected override void HandleMouseMoveRelative(Vector2 delta) - { - if (handlerBound) - { - // When bound, relative movement is reported via the WndProc handler below. - return; - } - - base.HandleMouseMoveRelative(delta); - } - - private unsafe IntPtr onWndProcSDL2(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) - { - if (!Enabled.Value) - return IntPtr.Zero; - - if (message != Native.Input.WM_INPUT) - return IntPtr.Zero; - - if (Native.Input.IsTouchEvent(Native.Input.GetMessageExtraInfo())) - { - // sometimes GetMessageExtraInfo returns 0, so additionally, mouse.ExtraInformation is checked below. - // touch events are handled by TouchHandler - statistic_dropped_touch_inputs.Value++; - return IntPtr.Zero; - } - - int payloadSize = sizeof(RawInputData); - - Native.Input.GetRawInputData((IntPtr)lParam, RawInputCommand.Input, out var data, ref payloadSize, sizeof(RawInputHeader)); - - if (data.Header.Type != RawInputType.Mouse) - return IntPtr.Zero; - - var mouse = data.Mouse; - - // `ExtraInformation` doesn't have the MI_WP_SIGNATURE set, so we have to rely solely on the touch flag. - if (Native.Input.HasTouchFlag(mouse.ExtraInformation)) - { - statistic_dropped_touch_inputs.Value++; - return IntPtr.Zero; - } - - //TODO: this isn't correct. - if (mouse.ExtraInformation > 0) - { - statistic_inputs_with_extra_information.Value++; - - // i'm not sure if there is a valid case where we need to handle packets with this present - // but the osu!tablet fires noise events with non-zero values, which we want to ignore. - // return IntPtr.Zero; - } - - var position = new Vector2(mouse.LastX, mouse.LastY); - float sensitivity = (float)Sensitivity.Value; - - if (mouse.Flags.HasFlagFast(RawMouseFlags.MoveAbsolute)) - { - var screenRect = mouse.Flags.HasFlagFast(RawMouseFlags.VirtualDesktop) ? Native.Input.VirtualScreenRect : new Rectangle(window.Position, window.ClientSize); - - Vector2 screenSize = new Vector2(screenRect.Width, screenRect.Height); - - if (mouse.LastX == 0 && mouse.LastY == 0) - { - // not sure if this is the case for all tablets, but on osu!tablet these can appear and are noise. - return IntPtr.Zero; - } - - // i am not sure what this 64 flag is, but it's set on the osu!tablet at very least. - // using it here as a method of determining where the coordinate space is incorrect. - if (((int)mouse.Flags & 64) == 0) - { - position /= raw_input_coordinate_space; - position *= screenSize; - } - - if (Sensitivity.Value != 1) - { - // apply absolute sensitivity adjustment from the centre of the screen area. - Vector2 halfScreenSize = (screenSize / 2); - - position -= halfScreenSize; - position *= (float)Sensitivity.Value; - position += halfScreenSize; - } - - // map from screen to client coordinate space. - // not using Window's PointToClient implementation to keep floating point precision here. - position -= new Vector2(window.Position.X, window.Position.Y); - position *= window.Scale; - - PendingInputs.Enqueue(new MousePositionAbsoluteInput { Position = position }); - statistic_absolute_events.Value++; - } - else - { - PendingInputs.Enqueue(new MousePositionRelativeInput { Delta = new Vector2(mouse.LastX, mouse.LastY) * sensitivity }); - statistic_relative_events.Value++; - } - - return IntPtr.Zero; - } } } diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs index bd13d7b174..2d941ec173 100644 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs +++ b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL2.cs @@ -2,15 +2,30 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Drawing; +using osu.Framework.Extensions.EnumExtensions; +using osu.Framework.Input.StateChanges; +using osu.Framework.Platform.Windows.Native; +using osu.Framework.Statistics; +using osuTK; using static SDL2.SDL; namespace osu.Framework.Platform.Windows { internal partial class WindowsMouseHandler { + private static readonly GlobalStatistic statistic_relative_events = GlobalStatistics.Get(StatisticGroupFor(), "Relative events"); + private static readonly GlobalStatistic statistic_absolute_events = GlobalStatistics.Get(StatisticGroupFor(), "Absolute events"); + private static readonly GlobalStatistic statistic_dropped_touch_inputs = GlobalStatistics.Get(StatisticGroupFor(), "Dropped native touch inputs"); + + private static readonly GlobalStatistic statistic_inputs_with_extra_information = + GlobalStatistics.Get(StatisticGroupFor(), "Native inputs with ExtraInformation"); + + private const int raw_input_coordinate_space = 65535; + private SDL_WindowsMessageHook sdl2Callback = null!; - private bool bindHandlerSDL2(GameHost host) + private void initialiseSDL2(GameHost host) { // ReSharper disable once ConvertClosureToMethodGroup sdl2Callback = (ptr, wnd, u, param, l) => onWndProcSDL2(ptr, wnd, u, param, l); @@ -19,8 +34,109 @@ private bool bindHandlerSDL2(GameHost host) { host.InputThread.Scheduler.Add(() => SDL_SetWindowsMessageHook(enabled.NewValue ? sdl2Callback : null, IntPtr.Zero)); }, true); + } + + protected override void HandleMouseMoveRelative(Vector2 delta) + { + if (window is SDL2WindowsWindow) + { + // on SDL2, relative movement is reported via the WndProc handler below. + return; + } + + base.HandleMouseMoveRelative(delta); + } + + private unsafe IntPtr onWndProcSDL2(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam) + { + if (!Enabled.Value) + return IntPtr.Zero; + + if (message != Native.Input.WM_INPUT) + return IntPtr.Zero; + + if (Native.Input.IsTouchEvent(Native.Input.GetMessageExtraInfo())) + { + // sometimes GetMessageExtraInfo returns 0, so additionally, mouse.ExtraInformation is checked below. + // touch events are handled by TouchHandler + statistic_dropped_touch_inputs.Value++; + return IntPtr.Zero; + } + + int payloadSize = sizeof(RawInputData); + + Native.Input.GetRawInputData((IntPtr)lParam, RawInputCommand.Input, out var data, ref payloadSize, sizeof(RawInputHeader)); + + if (data.Header.Type != RawInputType.Mouse) + return IntPtr.Zero; + + var mouse = data.Mouse; + + // `ExtraInformation` doesn't have the MI_WP_SIGNATURE set, so we have to rely solely on the touch flag. + if (Native.Input.HasTouchFlag(mouse.ExtraInformation)) + { + statistic_dropped_touch_inputs.Value++; + return IntPtr.Zero; + } + + //TODO: this isn't correct. + if (mouse.ExtraInformation > 0) + { + statistic_inputs_with_extra_information.Value++; + + // i'm not sure if there is a valid case where we need to handle packets with this present + // but the osu!tablet fires noise events with non-zero values, which we want to ignore. + // return IntPtr.Zero; + } + + var position = new Vector2(mouse.LastX, mouse.LastY); + float sensitivity = (float)Sensitivity.Value; + + if (mouse.Flags.HasFlagFast(RawMouseFlags.MoveAbsolute)) + { + var screenRect = mouse.Flags.HasFlagFast(RawMouseFlags.VirtualDesktop) ? Native.Input.VirtualScreenRect : new Rectangle(window.Position, window.ClientSize); + + Vector2 screenSize = new Vector2(screenRect.Width, screenRect.Height); + + if (mouse.LastX == 0 && mouse.LastY == 0) + { + // not sure if this is the case for all tablets, but on osu!tablet these can appear and are noise. + return IntPtr.Zero; + } + + // i am not sure what this 64 flag is, but it's set on the osu!tablet at very least. + // using it here as a method of determining where the coordinate space is incorrect. + if (((int)mouse.Flags & 64) == 0) + { + position /= raw_input_coordinate_space; + position *= screenSize; + } + + if (Sensitivity.Value != 1) + { + // apply absolute sensitivity adjustment from the centre of the screen area. + Vector2 halfScreenSize = (screenSize / 2); + + position -= halfScreenSize; + position *= (float)Sensitivity.Value; + position += halfScreenSize; + } + + // map from screen to client coordinate space. + // not using Window's PointToClient implementation to keep floating point precision here. + position -= new Vector2(window.Position.X, window.Position.Y); + position *= window.Scale; + + PendingInputs.Enqueue(new MousePositionAbsoluteInput { Position = position }); + statistic_absolute_events.Value++; + } + else + { + PendingInputs.Enqueue(new MousePositionRelativeInput { Delta = new Vector2(mouse.LastX, mouse.LastY) * sensitivity }); + statistic_relative_events.Value++; + } - return true; + return IntPtr.Zero; } } } diff --git a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs b/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs deleted file mode 100644 index 0d67175e35..0000000000 --- a/osu.Framework/Platform/Windows/WindowsMouseHandler_SDL3.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Framework.Platform.Windows -{ - internal partial class WindowsMouseHandler - { - private bool bindHandlerSDL3(GameHost host) - { - return false; - } - } -} From 47efee589dff2a474eac01c866ead2f2b4f7f2d3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 May 2024 15:43:13 +0900 Subject: [PATCH 059/140] Add SDL2 initialisation string --- osu.Framework/Platform/SDL2/SDL2Window.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Framework/Platform/SDL2/SDL2Window.cs b/osu.Framework/Platform/SDL2/SDL2Window.cs index 676501edf4..7a62f84d7c 100644 --- a/osu.Framework/Platform/SDL2/SDL2Window.cs +++ b/osu.Framework/Platform/SDL2/SDL2Window.cs @@ -192,6 +192,11 @@ protected SDL2Window(GraphicsSurfaceType surfaceType, string appName) throw new InvalidOperationException($"Failed to initialise SDL: {SDL_GetError()}"); } + SDL_GetVersion(out SDL_version version); + Logger.Log($@"SDL2 Initialized + SDL2 Version: {version.major}.{version.minor}.{version.patch} + SDL2 Revision: {SDL_GetRevision()}"); + SDL_LogSetPriority((int)SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG); SDL_LogSetOutputFunction(logOutputDelegate = logOutput, IntPtr.Zero); From dc4b11f973cdfa9b13aac3a6c38858521d99913e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 May 2024 15:43:33 +0900 Subject: [PATCH 060/140] Add window type to frame statistics display --- .../Graphics/Performance/PerformanceOverlay.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Framework/Graphics/Performance/PerformanceOverlay.cs b/osu.Framework/Graphics/Performance/PerformanceOverlay.cs index 1d0f58b061..598a2e51f1 100644 --- a/osu.Framework/Graphics/Performance/PerformanceOverlay.cs +++ b/osu.Framework/Graphics/Performance/PerformanceOverlay.cs @@ -12,6 +12,8 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Platform; +using osu.Framework.Platform.SDL2; +using osu.Framework.Platform.SDL3; using osu.Framework.Threading; using osuTK; using osuTK.Graphics; @@ -231,6 +233,17 @@ private void updateInfoText() addHeader("Mode:"); addValue(configWindowMode.ToString()); + switch (host.Window) + { + case SDL3Window: + addValue(" (SDL3)"); + break; + + case SDL2Window: + addValue(" (SDL2)"); + break; + } + void addHeader(string text) => infoText.AddText($"{text} ", cp => { cp.Padding = new MarginPadding { Left = 5 }; From ae266181b3d91cedb163482e1b1d0159dc530225 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 May 2024 17:54:07 +0900 Subject: [PATCH 061/140] Revert "Merge pull request #6275 from smoogipoo/update-veldrid" This reverts commit 812e5290b88142271040fb4114c7e0d5c4ac2975, reversing changes made to 9c6dee5e854d396f9c223ab9f722995d3fa7a193. --- .../Rendering/Deferred/DeferredFrameBuffer.cs | 2 +- .../Veldrid/Buffers/VeldridFrameBuffer.cs | 4 ++-- .../Veldrid/Pipelines/GraphicsPipeline.cs | 4 ++-- .../Graphics/Veldrid/Textures/VeldridTexture.cs | 4 ++-- .../Veldrid/Textures/VeldridVideoTexture.cs | 4 ++-- osu.Framework/Graphics/Veldrid/VeldridDevice.cs | 4 ++-- .../Graphics/Veldrid/VeldridExtensions.cs | 16 +++++++++------- osu.Framework/osu.Framework.csproj | 2 +- 8 files changed, 21 insertions(+), 19 deletions(-) diff --git a/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs b/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs index 4cdd71cf27..b055e35963 100644 --- a/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs +++ b/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs @@ -139,7 +139,7 @@ public void EnsureCreated() (uint)resourceSize.Y, 1, 1, - PixelFormat.R8G8B8A8UNorm, + PixelFormat.R8_G8_B8_A8_UNorm, TextureUsage.Sampled | TextureUsage.RenderTarget)), deferredFrameBuffer.renderer.Factory.CreateSampler( new SamplerDescription( diff --git a/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs b/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs index d14c0d4822..5481734ae9 100644 --- a/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs +++ b/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs @@ -47,7 +47,7 @@ public Vector2 Size } } - public VeldridFrameBuffer(VeldridRenderer renderer, PixelFormat[]? formats = null, SamplerFilter filteringMode = SamplerFilter.MinLinearMagLinearMipLinear) + public VeldridFrameBuffer(VeldridRenderer renderer, PixelFormat[]? formats = null, SamplerFilter filteringMode = SamplerFilter.MinLinear_MagLinear_MipLinear) { // todo: we probably want the arguments separated to "PixelFormat[] colorFormats, PixelFormat depthFormat". if (formats?.Length > 1) @@ -147,7 +147,7 @@ private class FrameBufferTexture : VeldridTexture { protected override TextureUsage Usages => base.Usages | TextureUsage.RenderTarget; - public FrameBufferTexture(VeldridRenderer renderer, SamplerFilter filteringMode = SamplerFilter.MinLinearMagLinearMipLinear) + public FrameBufferTexture(VeldridRenderer renderer, SamplerFilter filteringMode = SamplerFilter.MinLinear_MagLinear_MipLinear) : base(renderer, 1, 1, true, filteringMode) { BypassTextureUploadQueueing = true; diff --git a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs index 76ff428298..c2be61aef9 100644 --- a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs +++ b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs @@ -27,8 +27,8 @@ internal class GraphicsPipeline : BasicPipeline private GraphicsPipelineDescription pipelineDesc = new GraphicsPipelineDescription { - RasterizerState = RasterizerStateDescription.CULL_NONE, - BlendState = BlendStateDescription.SINGLE_OVERRIDE_BLEND, + RasterizerState = RasterizerStateDescription.CullNone, + BlendState = BlendStateDescription.SingleOverrideBlend, ShaderSet = { VertexLayouts = new VertexLayoutDescription[1] } }; diff --git a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs index 8e50c8f5cb..f5b570bb4d 100644 --- a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs +++ b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs @@ -85,7 +85,7 @@ protected virtual TextureUsage Usages /// Whether manual mipmaps will be uploaded to the texture. If false, the texture will compute mipmaps automatically. /// The filtering mode. /// The colour to initialise texture levels with (in the case of sub region initial uploads). If null, no initialisation is provided out-of-the-box. - public VeldridTexture(IVeldridRenderer renderer, int width, int height, bool manualMipmaps = false, SamplerFilter filteringMode = SamplerFilter.MinLinearMagLinearMipLinear, + public VeldridTexture(IVeldridRenderer renderer, int width, int height, bool manualMipmaps = false, SamplerFilter filteringMode = SamplerFilter.MinLinear_MagLinear_MipLinear, Color4? initialisationColour = null) { this.manualMipmaps = manualMipmaps; @@ -458,7 +458,7 @@ protected virtual void DoUpload(ITextureUpload upload) { texture?.Dispose(); - var textureDescription = TextureDescription.Texture2D((uint)Width, (uint)Height, (uint)CalculateMipmapLevels(Width, Height), 1, PixelFormat.R8G8B8A8UNorm, Usages); + var textureDescription = TextureDescription.Texture2D((uint)Width, (uint)Height, (uint)CalculateMipmapLevels(Width, Height), 1, PixelFormat.R8_G8_B8_A8_UNorm, Usages); texture = Renderer.Factory.CreateTexture(ref textureDescription); // todo: we may want to look into not having to allocate chunks of zero byte region for initialising textures diff --git a/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs b/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs index 0243cdb675..d2d3d4b076 100644 --- a/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs +++ b/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs @@ -48,13 +48,13 @@ protected override void DoUpload(ITextureUpload upload) resourceList[i] = new VeldridTextureResources ( - Renderer.Factory.CreateTexture(TextureDescription.Texture2D((uint)width, (uint)height, 1, 1, PixelFormat.R8UNorm, Usages)), + Renderer.Factory.CreateTexture(TextureDescription.Texture2D((uint)width, (uint)height, 1, 1, PixelFormat.R8_UNorm, Usages)), Renderer.Factory.CreateSampler(new SamplerDescription { AddressModeU = SamplerAddressMode.Clamp, AddressModeV = SamplerAddressMode.Clamp, AddressModeW = SamplerAddressMode.Clamp, - Filter = SamplerFilter.MinLinearMagLinearMipLinear, + Filter = SamplerFilter.MinLinear_MagLinear_MipLinear, MinimumLod = 0, MaximumLod = IRenderer.MAX_MIPMAP_LEVELS, MaximumAnisotropy = 0, diff --git a/osu.Framework/Graphics/Veldrid/VeldridDevice.cs b/osu.Framework/Graphics/Veldrid/VeldridDevice.cs index bbb5d1de48..365aaf7506 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridDevice.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridDevice.cs @@ -14,7 +14,7 @@ using SixLabors.ImageSharp.Processing; using Veldrid; using Veldrid.OpenGL; -using Veldrid.OpenGLBindings; +using Veldrid.OpenGLBinding; namespace osu.Framework.Graphics.Veldrid { @@ -106,7 +106,7 @@ public VeldridDevice(IGraphicsSurface graphicsSurface) var options = new GraphicsDeviceOptions { HasMainSwapchain = true, - SwapchainDepthFormat = PixelFormat.R16UNorm, + SwapchainDepthFormat = PixelFormat.R16_UNorm, SyncToVerticalBlank = true, ResourceBindingModel = ResourceBindingModel.Improved, }; diff --git a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs index 272452a14d..0ef17a07da 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs @@ -14,13 +14,15 @@ using SharpGen.Runtime; using Veldrid; using Veldrid.MetalBindings; -using Veldrid.OpenGLBindings; +using Veldrid.OpenGLBinding; using Vortice.Direct3D11; using Vortice.DXGI; using Vulkan; +using GetPName = Veldrid.OpenGLBinding.GetPName; using GraphicsBackend = Veldrid.GraphicsBackend; using PrimitiveTopology = Veldrid.PrimitiveTopology; using StencilOperation = Veldrid.StencilOperation; +using StringName = Veldrid.OpenGLBinding.StringName; using VertexAttribPointerType = osuTK.Graphics.ES30.VertexAttribPointerType; namespace osu.Framework.Graphics.Veldrid @@ -123,19 +125,19 @@ public static PixelFormat[] ToPixelFormats(this RenderBufferFormat[] renderBuffe switch (renderBufferFormats[i]) { case RenderBufferFormat.D16: - pixelFormats[i] = PixelFormat.R16UNorm; + pixelFormats[i] = PixelFormat.R16_UNorm; break; case RenderBufferFormat.D32: - pixelFormats[i] = PixelFormat.R32Float; + pixelFormats[i] = PixelFormat.R32_Float; break; case RenderBufferFormat.D24S8: - pixelFormats[i] = PixelFormat.D24UNormS8UInt; + pixelFormats[i] = PixelFormat.D24_UNorm_S8_UInt; break; case RenderBufferFormat.D32S8: - pixelFormats[i] = PixelFormat.D32FloatS8UInt; + pixelFormats[i] = PixelFormat.D32_Float_S8_UInt; break; default: @@ -151,10 +153,10 @@ public static SamplerFilter ToSamplerFilter(this TextureFilteringMode mode) switch (mode) { case TextureFilteringMode.Linear: - return SamplerFilter.MinLinearMagLinearMipLinear; + return SamplerFilter.MinLinear_MagLinear_MipLinear; case TextureFilteringMode.Nearest: - return SamplerFilter.MinPointMagPointMipPoint; + return SamplerFilter.MinPoint_MagPoint_MipPoint; default: throw new ArgumentOutOfRangeException(nameof(mode)); diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index bdd3d74dfd..782324da35 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -28,7 +28,7 @@ - + From 22b051bf2079a5321bf269086fed29304f056381 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 May 2024 21:10:28 +0900 Subject: [PATCH 062/140] Revert "Merge pull request #6294 from smoogipoo/rollback-veldrid" This reverts commit f8e09f12fa7efd0dfde31dfba5ba4a178c8fedfa, reversing changes made to 1f6b1575cbd92fd71b7f78ad24dd2ccca97ed107. --- .../Rendering/Deferred/DeferredFrameBuffer.cs | 2 +- .../Veldrid/Buffers/VeldridFrameBuffer.cs | 4 ++-- .../Veldrid/Pipelines/GraphicsPipeline.cs | 4 ++-- .../Graphics/Veldrid/Textures/VeldridTexture.cs | 4 ++-- .../Veldrid/Textures/VeldridVideoTexture.cs | 4 ++-- osu.Framework/Graphics/Veldrid/VeldridDevice.cs | 4 ++-- .../Graphics/Veldrid/VeldridExtensions.cs | 16 +++++++--------- osu.Framework/osu.Framework.csproj | 2 +- 8 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs b/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs index b055e35963..4cdd71cf27 100644 --- a/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs +++ b/osu.Framework/Graphics/Rendering/Deferred/DeferredFrameBuffer.cs @@ -139,7 +139,7 @@ public void EnsureCreated() (uint)resourceSize.Y, 1, 1, - PixelFormat.R8_G8_B8_A8_UNorm, + PixelFormat.R8G8B8A8UNorm, TextureUsage.Sampled | TextureUsage.RenderTarget)), deferredFrameBuffer.renderer.Factory.CreateSampler( new SamplerDescription( diff --git a/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs b/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs index 5481734ae9..d14c0d4822 100644 --- a/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs +++ b/osu.Framework/Graphics/Veldrid/Buffers/VeldridFrameBuffer.cs @@ -47,7 +47,7 @@ public Vector2 Size } } - public VeldridFrameBuffer(VeldridRenderer renderer, PixelFormat[]? formats = null, SamplerFilter filteringMode = SamplerFilter.MinLinear_MagLinear_MipLinear) + public VeldridFrameBuffer(VeldridRenderer renderer, PixelFormat[]? formats = null, SamplerFilter filteringMode = SamplerFilter.MinLinearMagLinearMipLinear) { // todo: we probably want the arguments separated to "PixelFormat[] colorFormats, PixelFormat depthFormat". if (formats?.Length > 1) @@ -147,7 +147,7 @@ private class FrameBufferTexture : VeldridTexture { protected override TextureUsage Usages => base.Usages | TextureUsage.RenderTarget; - public FrameBufferTexture(VeldridRenderer renderer, SamplerFilter filteringMode = SamplerFilter.MinLinear_MagLinear_MipLinear) + public FrameBufferTexture(VeldridRenderer renderer, SamplerFilter filteringMode = SamplerFilter.MinLinearMagLinearMipLinear) : base(renderer, 1, 1, true, filteringMode) { BypassTextureUploadQueueing = true; diff --git a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs index c2be61aef9..76ff428298 100644 --- a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs +++ b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs @@ -27,8 +27,8 @@ internal class GraphicsPipeline : BasicPipeline private GraphicsPipelineDescription pipelineDesc = new GraphicsPipelineDescription { - RasterizerState = RasterizerStateDescription.CullNone, - BlendState = BlendStateDescription.SingleOverrideBlend, + RasterizerState = RasterizerStateDescription.CULL_NONE, + BlendState = BlendStateDescription.SINGLE_OVERRIDE_BLEND, ShaderSet = { VertexLayouts = new VertexLayoutDescription[1] } }; diff --git a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs index f5b570bb4d..8e50c8f5cb 100644 --- a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs +++ b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs @@ -85,7 +85,7 @@ protected virtual TextureUsage Usages /// Whether manual mipmaps will be uploaded to the texture. If false, the texture will compute mipmaps automatically. /// The filtering mode. /// The colour to initialise texture levels with (in the case of sub region initial uploads). If null, no initialisation is provided out-of-the-box. - public VeldridTexture(IVeldridRenderer renderer, int width, int height, bool manualMipmaps = false, SamplerFilter filteringMode = SamplerFilter.MinLinear_MagLinear_MipLinear, + public VeldridTexture(IVeldridRenderer renderer, int width, int height, bool manualMipmaps = false, SamplerFilter filteringMode = SamplerFilter.MinLinearMagLinearMipLinear, Color4? initialisationColour = null) { this.manualMipmaps = manualMipmaps; @@ -458,7 +458,7 @@ protected virtual void DoUpload(ITextureUpload upload) { texture?.Dispose(); - var textureDescription = TextureDescription.Texture2D((uint)Width, (uint)Height, (uint)CalculateMipmapLevels(Width, Height), 1, PixelFormat.R8_G8_B8_A8_UNorm, Usages); + var textureDescription = TextureDescription.Texture2D((uint)Width, (uint)Height, (uint)CalculateMipmapLevels(Width, Height), 1, PixelFormat.R8G8B8A8UNorm, Usages); texture = Renderer.Factory.CreateTexture(ref textureDescription); // todo: we may want to look into not having to allocate chunks of zero byte region for initialising textures diff --git a/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs b/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs index d2d3d4b076..0243cdb675 100644 --- a/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs +++ b/osu.Framework/Graphics/Veldrid/Textures/VeldridVideoTexture.cs @@ -48,13 +48,13 @@ protected override void DoUpload(ITextureUpload upload) resourceList[i] = new VeldridTextureResources ( - Renderer.Factory.CreateTexture(TextureDescription.Texture2D((uint)width, (uint)height, 1, 1, PixelFormat.R8_UNorm, Usages)), + Renderer.Factory.CreateTexture(TextureDescription.Texture2D((uint)width, (uint)height, 1, 1, PixelFormat.R8UNorm, Usages)), Renderer.Factory.CreateSampler(new SamplerDescription { AddressModeU = SamplerAddressMode.Clamp, AddressModeV = SamplerAddressMode.Clamp, AddressModeW = SamplerAddressMode.Clamp, - Filter = SamplerFilter.MinLinear_MagLinear_MipLinear, + Filter = SamplerFilter.MinLinearMagLinearMipLinear, MinimumLod = 0, MaximumLod = IRenderer.MAX_MIPMAP_LEVELS, MaximumAnisotropy = 0, diff --git a/osu.Framework/Graphics/Veldrid/VeldridDevice.cs b/osu.Framework/Graphics/Veldrid/VeldridDevice.cs index 365aaf7506..bbb5d1de48 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridDevice.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridDevice.cs @@ -14,7 +14,7 @@ using SixLabors.ImageSharp.Processing; using Veldrid; using Veldrid.OpenGL; -using Veldrid.OpenGLBinding; +using Veldrid.OpenGLBindings; namespace osu.Framework.Graphics.Veldrid { @@ -106,7 +106,7 @@ public VeldridDevice(IGraphicsSurface graphicsSurface) var options = new GraphicsDeviceOptions { HasMainSwapchain = true, - SwapchainDepthFormat = PixelFormat.R16_UNorm, + SwapchainDepthFormat = PixelFormat.R16UNorm, SyncToVerticalBlank = true, ResourceBindingModel = ResourceBindingModel.Improved, }; diff --git a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs index 0ef17a07da..272452a14d 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs @@ -14,15 +14,13 @@ using SharpGen.Runtime; using Veldrid; using Veldrid.MetalBindings; -using Veldrid.OpenGLBinding; +using Veldrid.OpenGLBindings; using Vortice.Direct3D11; using Vortice.DXGI; using Vulkan; -using GetPName = Veldrid.OpenGLBinding.GetPName; using GraphicsBackend = Veldrid.GraphicsBackend; using PrimitiveTopology = Veldrid.PrimitiveTopology; using StencilOperation = Veldrid.StencilOperation; -using StringName = Veldrid.OpenGLBinding.StringName; using VertexAttribPointerType = osuTK.Graphics.ES30.VertexAttribPointerType; namespace osu.Framework.Graphics.Veldrid @@ -125,19 +123,19 @@ public static PixelFormat[] ToPixelFormats(this RenderBufferFormat[] renderBuffe switch (renderBufferFormats[i]) { case RenderBufferFormat.D16: - pixelFormats[i] = PixelFormat.R16_UNorm; + pixelFormats[i] = PixelFormat.R16UNorm; break; case RenderBufferFormat.D32: - pixelFormats[i] = PixelFormat.R32_Float; + pixelFormats[i] = PixelFormat.R32Float; break; case RenderBufferFormat.D24S8: - pixelFormats[i] = PixelFormat.D24_UNorm_S8_UInt; + pixelFormats[i] = PixelFormat.D24UNormS8UInt; break; case RenderBufferFormat.D32S8: - pixelFormats[i] = PixelFormat.D32_Float_S8_UInt; + pixelFormats[i] = PixelFormat.D32FloatS8UInt; break; default: @@ -153,10 +151,10 @@ public static SamplerFilter ToSamplerFilter(this TextureFilteringMode mode) switch (mode) { case TextureFilteringMode.Linear: - return SamplerFilter.MinLinear_MagLinear_MipLinear; + return SamplerFilter.MinLinearMagLinearMipLinear; case TextureFilteringMode.Nearest: - return SamplerFilter.MinPoint_MagPoint_MipPoint; + return SamplerFilter.MinPointMagPointMipPoint; default: throw new ArgumentOutOfRangeException(nameof(mode)); diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 782324da35..bdd3d74dfd 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -28,7 +28,7 @@ - + From bc2cd1a6888440e5eaf2c24e7e09134a9ecf75d4 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 22 May 2024 21:11:02 +0900 Subject: [PATCH 063/140] Update veldrid package --- osu.Framework/osu.Framework.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index bdd3d74dfd..e856c6b427 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -28,7 +28,7 @@ - + From f15b9c94d0ba224828cd8fe42a306815a8813bc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 22 May 2024 16:13:16 +0200 Subject: [PATCH 064/140] Update SDL3-CS Pulls in touch fixes, among others (haven't tested yet, just had the diff for breaking API changes locally from doing something else). --- osu.Framework/Platform/SDL3/SDL3Extensions.cs | 2 +- osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs | 6 +++--- osu.Framework/Platform/SDL3/SDL3Window.cs | 9 ++++----- osu.Framework/osu.Framework.csproj | 2 +- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Framework/Platform/SDL3/SDL3Extensions.cs b/osu.Framework/Platform/SDL3/SDL3Extensions.cs index 391e2c6ab5..2bd703ddb7 100644 --- a/osu.Framework/Platform/SDL3/SDL3Extensions.cs +++ b/osu.Framework/Platform/SDL3/SDL3Extensions.cs @@ -19,7 +19,7 @@ public static Key ToKey(this SDL_Keysym sdlKeysym) { // Apple devices don't have the notion of NumLock (they have a Clear key instead). // treat them as if they always have NumLock on (the numpad always performs its primary actions). - bool numLockOn = sdlKeysym.Mod.HasFlagFast(SDL_Keymod.SDL_KMOD_NUM) || RuntimeInfo.IsApple; + bool numLockOn = sdlKeysym.mod.HasFlagFast(SDL_Keymod.SDL_KMOD_NUM) || RuntimeInfo.IsApple; switch (sdlKeysym.scancode) { diff --git a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs index bf0030c0b1..3c7e0aaeab 100644 --- a/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs +++ b/osu.Framework/Platform/SDL3/SDL3GraphicsSurface.cs @@ -136,15 +136,15 @@ private void loadEntryPoints(GraphicsBindingsBase bindings) private IntPtr getProcAddress(string symbol) { const SDL_LogCategory error_category = SDL_LogCategory.SDL_LOG_CATEGORY_ERROR; - SDL_LogPriority oldPriority = SDL_LogGetPriority(error_category); + SDL_LogPriority oldPriority = SDL_GetLogPriority(error_category); // Prevent logging calls to SDL_GL_GetProcAddress() that fail on systems which don't have the requested symbol (typically macOS). - SDL_LogSetPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO); + SDL_SetLogPriority(error_category, SDL_LogPriority.SDL_LOG_PRIORITY_INFO); IntPtr ret = SDL_GL_GetProcAddress(symbol); // Reset the logging behaviour. - SDL_LogSetPriority(error_category, oldPriority); + SDL_SetLogPriority(error_category, oldPriority); return ret; } diff --git a/osu.Framework/Platform/SDL3/SDL3Window.cs b/osu.Framework/Platform/SDL3/SDL3Window.cs index d8fc645207..419b547816 100644 --- a/osu.Framework/Platform/SDL3/SDL3Window.cs +++ b/osu.Framework/Platform/SDL3/SDL3Window.cs @@ -161,13 +161,12 @@ protected SDL3Window(GraphicsSurfaceType surfaceType, string appName) throw new InvalidOperationException($"Failed to initialise SDL: {SDL_GetError()}"); } - SDL_Version version; - SDL_GetVersion(&version); + int version = SDL_GetVersion(); Logger.Log($@"SDL3 Initialized - SDL3 Version: {version.major}.{version.minor}.{version.patch} + SDL3 Version: {SDL_VERSIONNUM_MAJOR(version)}.{SDL_VERSIONNUM_MINOR(version)}.{SDL_VERSIONNUM_MICRO(version)} SDL3 Revision: {SDL_GetRevision()}"); - SDL_LogSetPriority(SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG); + SDL_SetLogPriority(SDL_LogCategory.SDL_LOG_CATEGORY_ERROR, SDL_LogPriority.SDL_LOG_PRIORITY_DEBUG); SDL_SetLogOutputFunction(&logOutput, IntPtr.Zero); SDL_SetEventFilter(&eventFilter, ObjectHandle.Handle); @@ -466,7 +465,7 @@ private void pollSDLEvents() do { - eventsRead = SDL_PeepEvents(events, SDL_eventaction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST); + eventsRead = SDL_PeepEvents(events, SDL_EventAction.SDL_GETEVENT, SDL_EventType.SDL_EVENT_FIRST, SDL_EventType.SDL_EVENT_LAST); for (int i = 0; i < eventsRead; i++) HandleEvent(events[i]); } while (eventsRead == events_per_peep); diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index e856c6b427..3ba41eb0bd 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -40,7 +40,7 @@ - + NUnit.Framework.AssertionException : : at osu.Framework.Testing.TestScene.checkForErrors() in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Testing/TestScene.cs:line 502 at osu.Framework.Testing.TestScene.UseTestSceneRunnerAttribute.AfterTest(ITest test) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Testing/TestScene.cs:line 563 at NUnit.Framework.Internal.Commands.TestActionCommand.<>c__DisplayClass0_0.<.ctor>b__1(TestExecutionContext context) at NUnit.Framework.Internal.Commands.BeforeAndAfterTestCommand.<>c__DisplayClass1_0.b__1() at NUnit.Framework.Internal.Commands.DelegatingTestCommand.RunTestMethodInThreadAbortSafeZone(TestExecutionContext context, Action action) --AssertionException at osu.Framework.Logging.ThrowingTraceListener.Fail(String message1, String message2) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Logging/ThrowingTraceListener.cs:line 27 at System.Diagnostics.TraceInternal.Fail(String message, String detailMessage) at System.Diagnostics.Debug.Fail(String message, String detailMessage) at osu.Framework.Input.Bindings.KeyCombination.IsPressed(ImmutableArray`1 pressedPhysicalKeys, InputKey candidateKey) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyCombination.cs:line 206 at osu.Framework.Input.Bindings.KeyCombination.IsPressed(KeyCombination pressedKeys, InputState inputState, KeyCombinationMatchingMode matchingMode) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyCombination.cs:line 105 at osu.Framework.Input.Bindings.KeyBindingContainer`1.handleNewPressed(InputState state, InputKey newKey, Nullable`1 scrollDelta, Boolean isPrecise) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyBindingContainer.cs:line 227 at osu.Framework.Input.Bindings.KeyBindingContainer`1.Handle(UIEvent e) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Input/Bindings/KeyBindingContainer.cs:line 125 at osu.Framework.Graphics.Drawable.OnMouseDown(MouseDownEvent e) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Graphics/Drawable.cs:line 2157 at osu.Framework.Graphics.Drawable.TriggerEvent(UIEvent e) in /home/dachb/Documents/opensource/osu-framework/osu.Framework/Graphics/Drawable.cs:line 2033 This is happening game-side when a keybinding is vacated, at which point it is represented by `KeyCombination.none` or equivalent, i.e. a `KeyCombination` with its only key being `InputKey.None`. `InputKey.None` is defined to be neither physical or virtual, which trips the aforementioned assertion. To bypass that entire debacle altogether, add an early return path that just returns false if the key combination being tested for being pressed is equivalent to `none`. (It cannot be a reference equality check, I checked against game. Sequence equality is required. Something in game is probably instantiating anew. Probably something to do with how keybindings are materialised from realm.) --- osu.Framework/Input/Bindings/KeyCombination.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Framework/Input/Bindings/KeyCombination.cs b/osu.Framework/Input/Bindings/KeyCombination.cs index 72c659fb39..4a178fd253 100644 --- a/osu.Framework/Input/Bindings/KeyCombination.cs +++ b/osu.Framework/Input/Bindings/KeyCombination.cs @@ -102,6 +102,9 @@ public bool IsPressed(KeyCombination pressedKeys, InputState inputState, KeyComb if (Keys == pressedKeys.Keys) // Fast test for reference equality of underlying array return true; + if (Keys.SequenceEqual(none)) + return false; + return ContainsAll(Keys, pressedKeys.Keys, matchingMode); } From 20a614d73732e99df4a22ff1e821006588a370b3 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 28 May 2024 20:18:09 +0900 Subject: [PATCH 081/140] Upgrade SDL3 package --- osu.Framework.Android/osu.Framework.Android.csproj | 2 +- osu.Framework/osu.Framework.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework.Android/osu.Framework.Android.csproj b/osu.Framework.Android/osu.Framework.Android.csproj index 1f68f2de0d..2ecd2071a7 100644 --- a/osu.Framework.Android/osu.Framework.Android.csproj +++ b/osu.Framework.Android/osu.Framework.Android.csproj @@ -18,7 +18,7 @@ - + diff --git a/osu.Framework/osu.Framework.csproj b/osu.Framework/osu.Framework.csproj index 3ba41eb0bd..99d9a131d9 100644 --- a/osu.Framework/osu.Framework.csproj +++ b/osu.Framework/osu.Framework.csproj @@ -40,7 +40,7 @@ - +