Skip to content

Commit f1880f0

Browse files
committed
Push latest work on SDL backend
1 parent 2080e1b commit f1880f0

26 files changed

Lines changed: 1045 additions & 180 deletions

.config/dotnet-tools.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
"isRoot": true,
44
"tools": {
55
"csharpier": {
6-
"version": "0.30.6",
6+
"version": "1.0.1",
77
"commands": [
8-
"dotnet-csharpier"
8+
"csharpier"
99
],
1010
"rollForward": false
1111
}

sources/Input/Input/CustomCursor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public readonly ref struct CustomCursor
1616
public int Height { get; init; }
1717

1818
/// <summary>
19-
/// The row-major 32-bit RGBA pixel data (i.e. 8 bytes for each colour component).
19+
/// The row-major 32-bit RGBA pixel data (i.e. 8 bits for each colour component).
2020
/// </summary>
2121
public ReadOnlySpan<int> Data { get; init; } // Rgba32
22-
}
22+
}

sources/Input/Input/DualReadOnlyList.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,24 @@ namespace Silk.NET.Input;
66
/// Represents a list that has exactly two elements.
77
/// </summary>
88
/// <typeparam name="T">The element type.</typeparam>
9-
public readonly struct DualReadOnlyList<T> : IReadOnlyList<T>
9+
public readonly struct DualReadOnlyList<T>(T left, T right) : IReadOnlyList<T>
1010
{
11+
/// <summary>
12+
/// Creates a copy of the given list.
13+
/// </summary>
14+
/// <param name="other">The list.</param>
15+
public DualReadOnlyList(DualReadOnlyList<T> other)
16+
: this(other.Left, other.Right) { }
17+
1118
/// <summary>
1219
/// The first/leftmost element.
1320
/// </summary>
14-
public readonly T Left;
21+
public readonly T Left = left;
1522

1623
/// <summary>
1724
/// The second/rightmost element.
1825
/// </summary>
19-
public readonly T Right;
26+
public readonly T Right = right;
2027

2128
/// <inheritdoc />
2229
public IEnumerator<T> GetEnumerator()
@@ -31,9 +38,11 @@ public IEnumerator<T> GetEnumerator()
3138
public int Count => 2;
3239

3340
/// <inheritdoc />
34-
public T this[int index] => index switch {
35-
0 => Left,
36-
1 => Right,
37-
_ => throw new IndexOutOfRangeException()
38-
};
39-
}
41+
public T this[int index] =>
42+
index switch
43+
{
44+
0 => Left,
45+
1 => Right,
46+
_ => throw new IndexOutOfRangeException(),
47+
};
48+
}

sources/Input/Input/GamepadState.cs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,35 @@ namespace Silk.NET.Input;
55
/// <summary>
66
/// Contains user input received from an <see cref="IGamepad"/>.
77
/// </summary>
8-
public class GamepadState
8+
public class GamepadState(
9+
ButtonReadOnlyList<JoystickButton> buttons,
10+
DualReadOnlyList<Vector2> thumbsticks,
11+
DualReadOnlyList<float> triggers
12+
)
913
{
14+
/// <summary>
15+
/// Clones the given state. This is useful for creating an immutable copy of state from a mutable one.
16+
/// </summary>
17+
/// <param name="other">The other state.</param>
18+
public GamepadState(GamepadState other)
19+
: this(
20+
new ButtonReadOnlyList<JoystickButton>(other.Buttons),
21+
new DualReadOnlyList<Vector2>(other.Thumbsticks),
22+
new DualReadOnlyList<float>(other.Triggers)
23+
) { }
24+
1025
/// <summary>
1126
/// Gets the gamepad button state denoting the buttons being pressed or depressed.
1227
/// </summary>
13-
public ButtonReadOnlyList<JoystickButton> Buttons { get; }
28+
public ButtonReadOnlyList<JoystickButton> Buttons { get; } = buttons;
1429

1530
/// <summary>
1631
/// Gets the state of the twin sticks on the gamepad.
1732
/// </summary>
18-
public DualReadOnlyList<Vector2> Thumbsticks { get; }
33+
public DualReadOnlyList<Vector2> Thumbsticks { get; internal set; } = thumbsticks;
1934

2035
/// <summary>
2136
/// Gets the state of the triggers on the gamepad.
2237
/// </summary>
23-
public DualReadOnlyList<float> Triggers { get; }
24-
}
38+
public DualReadOnlyList<float> Triggers { get; internal set; } = triggers;
39+
}

sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ public static partial IInputBackend CreateInputBackend(this INativeWindow window
2323
);
2424
}
2525

26-
return new SdlInputBackend(window, info);
26+
return new SdlInputBackend(info);
2727
}
2828
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Silk.NET.Input.SDL3;
7+
8+
/// <summary>
9+
/// A base class for SDL input devices that operate in terms of a window's or DWMs bounds.
10+
/// </summary>
11+
/// <param name="backend">The backend.</param>
12+
internal abstract class SdlBoundedPointerDevice(SdlInputBackend backend)
13+
: SdlDevice(backend),
14+
IPointerDevice
15+
{
16+
public abstract PointerState State { get; }
17+
18+
[field: MaybeNull]
19+
public virtual IReadOnlyList<IPointerTarget> Targets =>
20+
field ??= [Backend.BoundedPointerTarget];
21+
22+
/// <summary>
23+
/// Determines whether the <see cref="SdlBoundedPointerTarget"/> should interpret <see cref="PointerState.Points"/>
24+
/// as being bounded points. For all devices supported by this backend, only one target is supported at a time
25+
/// today.
26+
/// </summary>
27+
public virtual bool IsBounded => true;
28+
29+
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
30+
public InputMarshal.ListOwner<TargetPoint> BoundedPoints =>
31+
field.List.Data is null ? field = InputMarshal.CreateList<TargetPoint>() : field;
32+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Numerics;
5+
using Silk.NET.Maths;
6+
using Silk.NET.SDL;
7+
8+
namespace Silk.NET.Input.SDL3;
9+
10+
internal class SdlBoundedPointerTarget(SdlInputBackend backend) : IPointerTarget
11+
{
12+
internal SdlInputBackend Backend { get; } = backend;
13+
private Box2D<float> Bounds2D { get; set; }
14+
15+
public Box3D<float> Bounds =>
16+
new(new Vector3D<float>(Bounds2D.Min, 0), new Vector3D<float>(Bounds2D.Max, 1));
17+
18+
public static Box2D<float> CalculateBounds(ISdl sdl)
19+
{
20+
var minX = float.PositiveInfinity;
21+
var minY = float.PositiveInfinity;
22+
var maxX = float.NegativeInfinity;
23+
var maxY = float.NegativeInfinity;
24+
var displayCount = 0;
25+
var displays = sdl.GetDisplays(displayCount.AsRef());
26+
if (displays == nullptr)
27+
{
28+
// Looks like we can't support windowed mouse input.
29+
sdl.ClearError();
30+
return default;
31+
}
32+
33+
if (displayCount == 0) // ???
34+
{
35+
sdl.Free((Ref)displays);
36+
return default;
37+
}
38+
39+
for (var i = 0; i < displayCount; i++)
40+
{
41+
Rect rect = default;
42+
if (!sdl.GetDisplayBounds(displays[(nuint)i], rect.AsRef()))
43+
{
44+
return default;
45+
}
46+
47+
minX = float.Min(minX, rect.X);
48+
minY = float.Min(minY, rect.Y);
49+
maxX = float.Max(maxX, rect.X + rect.W);
50+
maxY = float.Max(maxY, rect.Y + rect.H);
51+
}
52+
53+
sdl.Free((Ref)displays);
54+
if (minX <= maxX && minY <= maxY)
55+
{
56+
return new Box2D<float>(minX, minY, maxX, maxY);
57+
}
58+
59+
return default;
60+
}
61+
62+
public int GetPointCount(IPointerDevice pointer)
63+
{
64+
if (pointer is not SdlBoundedPointerDevice { IsBounded: true } device)
65+
{
66+
return 0;
67+
}
68+
69+
if (device.Backend == Backend)
70+
{
71+
return Bounds != default ? device.BoundedPoints.List.Count : 0;
72+
}
73+
74+
return device.Backend.BoundedPointerTarget.GetPointCount(pointer);
75+
}
76+
77+
public TargetPoint GetPoint(IPointerDevice pointer, int point)
78+
{
79+
if (
80+
pointer is not SdlBoundedPointerDevice { IsBounded: true } device
81+
|| point < 0
82+
|| point >= device.BoundedPoints.List.Count
83+
)
84+
{
85+
return default;
86+
}
87+
88+
if (device.Backend != Backend)
89+
{
90+
return device.Backend.BoundedPointerTarget.GetPoint(pointer, point);
91+
}
92+
93+
return Bounds != default ? device.BoundedPoints.List[point] : default;
94+
}
95+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Silk.NET.Input.SDL3;
5+
6+
internal abstract class SdlDevice(SdlInputBackend backend) : IInputDevice
7+
{
8+
public bool Equals(IInputDevice? other) =>
9+
other?.GetType() == GetType()
10+
&& other.Id == Id
11+
&& other is SdlBoundedPointerDevice dev
12+
&& dev.Backend.Sdl == Backend.Sdl;
13+
14+
public abstract IntPtr Id { get; }
15+
public abstract string Name { get; }
16+
public SdlInputBackend Backend { get; } = backend;
17+
}
Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,125 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Numerics;
5+
using Silk.NET.SDL;
6+
47
namespace Silk.NET.Input.SDL3;
58

6-
internal class SdlGamepad : IGamepad
9+
internal class SdlGamepad : SdlDevice, IGamepad, IDisposable
710
{
8-
public bool Equals(IInputDevice? other) => throw new NotImplementedException();
11+
private readonly GamepadHandle _gamepad;
12+
13+
private static JoystickButton? GetSilkButton(GamepadButton btn) =>
14+
btn switch
15+
{
16+
GamepadButton.South => JoystickButton.ButtonDown,
17+
GamepadButton.East => JoystickButton.ButtonRight,
18+
GamepadButton.West => JoystickButton.ButtonLeft,
19+
GamepadButton.North => JoystickButton.ButtonUp,
20+
GamepadButton.Back => JoystickButton.Back,
21+
GamepadButton.Guide => JoystickButton.Home,
22+
GamepadButton.Start => JoystickButton.Start,
23+
GamepadButton.LeftStick => JoystickButton.LeftStick,
24+
GamepadButton.RightStick => JoystickButton.RightStick,
25+
GamepadButton.LeftShoulder => JoystickButton.LeftBumper,
26+
GamepadButton.RightShoulder => JoystickButton.RightBumper,
27+
GamepadButton.DpadUp => JoystickButton.DPadUp,
28+
GamepadButton.DpadDown => JoystickButton.DPadDown,
29+
GamepadButton.DpadLeft => JoystickButton.DPadLeft,
30+
GamepadButton.DpadRight => JoystickButton.DPadRight,
31+
// TODO not exposed today
32+
_ => null,
33+
};
34+
35+
public SdlGamepad(SdlInputBackend backend, uint joystickId)
36+
: base(backend)
37+
{
38+
_gamepad = backend.Sdl.OpenGamepad(joystickId);
39+
if (_gamepad == nullptr)
40+
{
41+
backend.Sdl.ThrowError();
42+
}
43+
44+
var buttons = InputMarshal.CreateList<Button<JoystickButton>>();
45+
for (var i = 0; i < (int)GamepadButton.Count; i++)
46+
{
47+
if (GetSilkButton((GamepadButton)i) is not { } btn)
48+
{
49+
continue;
50+
}
51+
52+
var isDown = backend.Sdl.GetGamepadButton(_gamepad, (GamepadButton)i);
53+
InputMarshal.SetButtonState(
54+
buttons,
55+
new Button<JoystickButton>(btn, isDown, isDown ? 1 : 0),
56+
true
57+
);
58+
}
59+
60+
// For thumbsticks, the state is a value ranging from -32768 (up/left) to 32767 (down/right).
61+
// Triggers range from 0 when released to 32767 when fully pressed, and never return a negative value. Note that
62+
// this differs from the value reported by the lower-level SDL_GetJoystickAxis(), which normally uses the full
63+
// range.
64+
var triggers = new DualReadOnlyList<float>(
65+
(float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.LeftTrigger) / short.MaxValue,
66+
(float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.RightTrigger) / short.MaxValue
67+
);
68+
var thumbsticks = new DualReadOnlyList<Vector2>(
69+
new Vector2(
70+
(float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Leftx) / short.MaxValue,
71+
(float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Lefty) / short.MaxValue
72+
),
73+
new Vector2(
74+
(float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Rightx) / short.MaxValue,
75+
(float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Righty) / short.MaxValue
76+
)
77+
);
78+
State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers);
79+
}
80+
81+
// TODO this is not spec compliant, we need to use a physical device ID
82+
public override unsafe nint Id => (nint)_gamepad.Handle;
83+
84+
public override string Name => Backend.Sdl.GetGamepadName(_gamepad).ReadToString();
85+
86+
public GamepadState State { get; }
87+
88+
// TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's
89+
// just left or right. The original intention was that this would be useful for things like 3D haptics, but what did
90+
// I know. The SDL people seem to have done a good job with their haptic API, let's see what we can do with it.
91+
// For now, this has the same implementation as it always has.
92+
public IReadOnlyList<IMotor> VibrationMotors =>
93+
_motors ??= [new SdlMotor(this, 0), new SdlMotor(this, 1)];
94+
95+
private IMotor[]? _motors;
96+
private ushort[]? _motorFrequencies;
97+
98+
internal ushort GetRumble(int motor) => (_motorFrequencies ??= [0, 0])[motor];
999

10-
public IntPtr Id => throw new NotImplementedException();
100+
internal void SetRumble(int motor, ushort value)
101+
{
102+
(_motorFrequencies ??= [0, 0])[motor] = value;
103+
if (
104+
!Backend.Sdl.RumbleGamepad(
105+
_gamepad,
106+
_motorFrequencies[0],
107+
_motorFrequencies[1],
108+
uint.MaxValue
109+
)
110+
)
111+
{
112+
Backend.Sdl.ThrowError();
113+
}
114+
}
11115

12-
public string Name => throw new NotImplementedException();
116+
private void ReleaseUnmanagedResources() => Backend.Sdl.CloseGamepad(_gamepad);
13117

14-
public GamepadState State => throw new NotImplementedException();
118+
public void Dispose()
119+
{
120+
ReleaseUnmanagedResources();
121+
GC.SuppressFinalize(this);
122+
}
15123

16-
public IReadOnlyList<IMotor> VibrationMotors => throw new NotImplementedException();
124+
~SdlGamepad() => ReleaseUnmanagedResources();
17125
}

0 commit comments

Comments
 (0)