diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 03efa87b74..b9f6941232 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,10 +3,11 @@
"isRoot": true,
"tools": {
"csharpier": {
- "version": "0.29.2",
+ "version": "1.0.1",
"commands": [
- "dotnet-csharpier"
- ]
+ "csharpier"
+ ],
+ "rollForward": false
}
}
}
\ No newline at end of file
diff --git a/Silk.NET.sln b/Silk.NET.sln
index 17850fdc6f..364d408733 100644
--- a/Silk.NET.sln
+++ b/Silk.NET.sln
@@ -102,6 +102,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Windowing", "Windowing", "{
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Windowing", "sources\Windowing\Windowing\Silk.NET.Windowing.csproj", "{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Input", "Input", "{33ED9765-8C36-4A9D-95E8-AF037FE104B3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Input", "sources\Input\Input\Silk.NET.Input.csproj", "{49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Input", "Input", "{4E0EF53A-76BC-4729-8E3B-4768E86E357E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silk.NET.Input.UnitTests", "tests\Input\Input\Silk.NET.Input.UnitTests.csproj", "{00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -168,6 +176,14 @@ Global
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -200,6 +216,10 @@ Global
{F16C0AB9-DE7E-4C09-9EE9-DAA8B8E935A6} = {EC4D7B06-D277-4411-BD7B-71A6D37683F0}
{FE4414F8-5370-445D-9F24-C3AD3223F299} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6}
{EF07CBB5-D253-4CA9-A5DA-8B3DF2B0DF8E} = {FE4414F8-5370-445D-9F24-C3AD3223F299}
+ {33ED9765-8C36-4A9D-95E8-AF037FE104B3} = {DD29EA8F-B1A6-45AA-8D2E-B38DA56D9EF6}
+ {49A42CE3-94C5-4239-B0FC-F1FF8D7AAADA} = {33ED9765-8C36-4A9D-95E8-AF037FE104B3}
+ {4E0EF53A-76BC-4729-8E3B-4768E86E357E} = {A5578D12-9E77-4647-8C22-0DBD17760BFF}
+ {00B9B6E6-776E-480C-B3ED-D6420C5B4E8E} = {4E0EF53A-76BC-4729-8E3B-4768E86E357E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {78D2CF6A-60A1-43E3-837B-00B73C9DA384}
diff --git a/docs/silk.net/diagnostics/ST0001.md b/docs/silk.net/diagnostics/ST0001.md
new file mode 100644
index 0000000000..f04906d857
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0001.md
@@ -0,0 +1,21 @@
+# ST0001 - ProcessClass failure
+
+## Overview
+
+This internal error was raised by SilkTouch when failing to generate an implementation for a binding at source
+generation time. It provided details regarding the exception that led to the entire native API class failing to have its
+implementation generated.
+
+| Attribute | Value |
+|--------------------|----------------------|
+| Diagnostic ID | ST0001 |
+| Title | ProcessClass failure |
+| Category | SilkTouch.Internal |
+| Default Severity | Error |
+| Enabled by Default | Yes |
+
+Example message: `ProcessClass failed. Exception: '...'`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0002.md b/docs/silk.net/diagnostics/ST0002.md
new file mode 100644
index 0000000000..89742a49a7
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0002.md
@@ -0,0 +1,21 @@
+# ST0002 - MethodClass failure
+
+## Overview
+
+This internal error was raised by SilkTouch when failing to generate an implementation for a binding at source
+generation time. It provided details regarding the exception that led to a specific native API method failing to have
+its implementation generated.
+
+| Attribute | Value |
+|--------------------|---------------------|
+| Diagnostic ID | ST0002 |
+| Title | MethodClass failure |
+| Category | SilkTouch.Internal |
+| Default Severity | Error |
+| Enabled by Default | Yes |
+
+Example message: `MethodClass failed. Exception: '...'`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0003.md b/docs/silk.net/diagnostics/ST0003.md
new file mode 100644
index 0000000000..d722f5f6d7
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0003.md
@@ -0,0 +1,20 @@
+# ST0003 - Silk.NET.Core is Missing
+
+## Overview
+
+This internal diagnostic was raised by SilkTouch when failing to generate an implementation for bindings at source
+generation time due to the binding project missing a reference to Silk.NET.Core.
+
+| Attribute | Value |
+|--------------------|---------------------|
+| Diagnostic ID | ST0003 |
+| Title | MethodClass failure |
+| Category | SilkTouch.Internal |
+| Default Severity | Info |
+| Enabled by Default | Yes |
+
+Example message: `Silk.NET.Core is missing from references. You should use SilkTouch with Silk.NET.Core`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0004.md b/docs/silk.net/diagnostics/ST0004.md
new file mode 100644
index 0000000000..4680d3faa3
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0004.md
@@ -0,0 +1,20 @@
+# ST0004 - Build Info
+
+## Overview
+
+This internal diagnostic was raised by SilkTouch when configured to do so. It provided diagnostic information relating
+to the performance and characteristics of SilkTouch's internals.
+
+| Attribute | Value |
+|--------------------|--------------------|
+| Diagnostic ID | ST0004 |
+| Title | Build Info |
+| Category | SilkTouch.Internal |
+| Default Severity | Warning |
+| Enabled by Default | Yes |
+
+Example message: `GCSlotCount: '127'. Time: '6437ms'`
+
+## Explanation & Solutions
+
+This functionality is no longer supported in 3.0, where this diagnostic is never raised.
diff --git a/docs/silk.net/diagnostics/ST0005.md b/docs/silk.net/diagnostics/ST0005.md
new file mode 100644
index 0000000000..8a7c730766
--- /dev/null
+++ b/docs/silk.net/diagnostics/ST0005.md
@@ -0,0 +1,15 @@
+# ST0005 - Intentionally Unstable API
+
+## Overview
+
+This diagnostic is raised when trying to use a Silk.NET API that has been marked with the `Experimental` attribute due
+to its API and/or ABI being unstable. When this diagnostic ID is used, it indicates that it is intentional that this is
+the case and that this API is extremely unlikely to ever graduate to a stable, versioned API.
+
+## Explanation & Solutions
+
+Typically, APIs meeting this description are internal APIs and are not intended for use outside of the assembly they're
+defined in. As a result, where this diagnostic is raised, you should cease use of this API or at least only continue if
+you can guarantee that you will never update Silk.NET ever again and that your downstream consumers, if applicable, will
+lock their version to the same version referenced by your project. However, please reconsider use of the API if this is
+the case.
diff --git a/eng/build/Silk.NET.NUKE.csproj b/eng/build/Silk.NET.NUKE.csproj
index 1f3ae0857e..2a1f1a1c28 100644
--- a/eng/build/Silk.NET.NUKE.csproj
+++ b/eng/build/Silk.NET.NUKE.csproj
@@ -18,4 +18,10 @@
+
+
+
+ Directory.Build\tests\Input\Input\Silk.NET.Input.UnitTests.csproj
+
+
diff --git a/sources/Input/Input/Button.cs b/sources/Input/Input/Button.cs
new file mode 100644
index 0000000000..1ae8875c65
--- /dev/null
+++ b/sources/Input/Input/Button.cs
@@ -0,0 +1,24 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a button the user can push.
+///
+/// The name of the button.
+/// Whether the user is pushing the button.
+///
+/// The pressure with which the user is pushing the button, where 0.0 is the smallest measurable pressure and
+/// 1.0 is the largest measurable pressure.
+///
+///
+/// The button type (e.g. , , etc).
+///
+public readonly record struct Button(T Name, bool IsDown, float Pressure)
+ where T : unmanaged, Enum
+{
+ ///
+ /// Collapses this struct into just its value.
+ ///
+ /// The button state.
+ /// The value.
+ public static implicit operator bool(Button state) => state.IsDown;
+}
diff --git a/sources/Input/Input/ButtonChangedEvent.cs b/sources/Input/Input/ButtonChangedEvent.cs
new file mode 100644
index 0000000000..b030762606
--- /dev/null
+++ b/sources/Input/Input/ButtonChangedEvent.cs
@@ -0,0 +1,21 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to a button state change (e.g. press, depress, etc).
+///
+/// The device on which the button being pressed or depressed resides.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+/// The new state of the button being pressed or depressed.
+/// The previous state of the button.
+/// The button type e.g. , , etc.
+public readonly record struct ButtonChangedEvent(
+ IButtonDevice Device,
+ long Timestamp,
+ Button Button,
+ Button Previous
+)
+ where T : unmanaged, Enum;
diff --git a/sources/Input/Input/ButtonReadOnlyList.cs b/sources/Input/Input/ButtonReadOnlyList.cs
new file mode 100644
index 0000000000..f93504197c
--- /dev/null
+++ b/sources/Input/Input/ButtonReadOnlyList.cs
@@ -0,0 +1,43 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+///
+/// An implementation of providing utility APIs for getting a
+/// given a button name , that is optimised for storing s with the
+/// given button name type using the most memory-efficient mechanism available.
+///
+///
+/// The button type (e.g. , , etc).
+///
+public struct ButtonReadOnlyList : IReadOnlyList>
+ where T : unmanaged, Enum
+{
+ private InputReadOnlyList> _list;
+
+ internal ButtonReadOnlyList(InputReadOnlyList> list) => _list = list;
+
+ ///
+ /// Creates an from a .
+ ///
+ /// The list to copy.
+ public ButtonReadOnlyList(IReadOnlyList> other) =>
+ InputMarshal.Clone(other).List.AsButtonList();
+
+ ///
+ /// Gets the state for the button with the given name.
+ ///
+ /// The button name.
+ public Button this[T name] => InputMarshal.GetButtonState(_list, name);
+
+ ///
+ public IEnumerator> GetEnumerator() => _list.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public int Count => _list.Count;
+
+ ///
+ public Button this[int index] => _list[index];
+}
diff --git a/sources/Input/Input/ConnectionEvent.cs b/sources/Input/Input/ConnectionEvent.cs
new file mode 100644
index 0000000000..2da787cc70
--- /dev/null
+++ b/sources/Input/Input/ConnectionEvent.cs
@@ -0,0 +1,13 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to a device connection or disconnection event.
+///
+/// The device that has disconnected or connected.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+/// Whether the device has connected (true ) or disconnected (false ).
+public readonly record struct ConnectionEvent(IInputDevice Device, long Timestamp, bool IsConnected);
\ No newline at end of file
diff --git a/sources/Input/Input/CursorModes.cs b/sources/Input/Input/CursorModes.cs
new file mode 100644
index 0000000000..a5ce021ea8
--- /dev/null
+++ b/sources/Input/Input/CursorModes.cs
@@ -0,0 +1,57 @@
+namespace Silk.NET.Input;
+
+///
+/// Enumerates the modes in which a mouse cursor can operate.
+///
+///
+/// implementations for implementations typically have two
+/// :
+///
+/// -
+///
Bounded
+///
+/// An that is bounded to the desktop environment i.e. the
+/// are not infinite and reflect the total screen space that is available to the
+/// running application in window coordinates. This is typically the sum of all monitor resolutions, with the positions
+/// being defined using an implementation-defined mechanism. The window bounds operate in this same coordinate space.
+/// It is highly unlikely that you will be unable to determine the individual points for multiple mice on this target,
+/// as desktop environments typically aggregate all movement from all mice into a single .
+/// This target is used for every cursor mode except .
+///
+///
+/// -
+///
Unbounded
+///
+/// An that is unbounded and operates in an arbitrary coordinate space. This target is used
+/// for raw mouse mode and points on this target represent the net mouse movement from a mouse. Implementations
+/// are more likely to be able to give multiple s for each mouse when this target is used. This
+/// target is used when the cursor mode is enabled. will
+/// represent an infinitely large unbounded target.
+///
+///
+///
+///
+[Flags]
+public enum CursorModes
+{
+ ///
+ /// The cursor is visible to the user and operating within the bounds of the desktop environment . The
+ /// coordinates received are in desktop coordinates, operating in the same coordinate space as the window
+ /// position/size.
+ ///
+ Normal = 1 << 0,
+
+ ///
+ /// The cursor is visible to the user but is constrained to the window's client area . The coordinates
+ /// received are in desktop coordinates, operating in the same coordinate space as the window position/size.
+ /// The bounded to the desktop environment is used.
+ ///
+ Confined = 1 << 1,
+
+ ///
+ /// The cursor is invisible to the user and is unconstrained/unbounded . The coordinates received are
+ /// arbitrary values that have no bounds representing the net mouse movement since entering into this cursor mode.
+ /// The unbounded is used. This is the equivalent of raw mouse mode .
+ ///
+ Unbounded = 1 << 2,
+}
\ No newline at end of file
diff --git a/sources/Input/Input/CursorStyles.cs b/sources/Input/Input/CursorStyles.cs
new file mode 100644
index 0000000000..65ecfc6f55
--- /dev/null
+++ b/sources/Input/Input/CursorStyles.cs
@@ -0,0 +1,61 @@
+namespace Silk.NET.Input;
+
+///
+/// Enumerates the cursor styles with which the desktop environment should render the cursor.
+///
+[Flags]
+public enum CursorStyles
+{
+ ///
+ /// The cursor should be rendered using its default image.
+ ///
+ Default,
+
+ ///
+ /// The cursor should be rendered using an arrow cursor image.
+ ///
+ Arrow = 1 << 0,
+
+ ///
+ /// The cursor should be rendered using an I-beam cursor image, which is used to show where the text cursor appears
+ /// when the mouse is clicked.
+ ///
+ IBeam = 1 << 1,
+
+ ///
+ /// The cursor should be rendered using a crosshair cursor image.
+ ///
+ Crosshair = 1 << 2,
+
+ ///
+ /// The cursor should be rendered using a hand cursor image, typically used when hovering over a web link.
+ ///
+ Hand = 1 << 3,
+
+ ///
+ /// The cursor should be rendered using a two-headed horizontal sizing cursor image.
+ ///
+ HResize = 1 << 4,
+
+ ///
+ /// The cursor should be rendered using a two-headed vertical sizing cursor image.
+ ///
+ VResize = 1 << 5,
+
+ ///
+ /// The cursor should not be rendered.
+ ///
+ ///
+ /// When is used, the cursor ceases to exist anyway. As such, while the
+ /// property may not reflect this (as it is retained across changes to
+ /// and just ignored when is used),
+ /// can be implied as being when
+ /// is used.
+ ///
+ Hidden = 1 << 6,
+
+ ///
+ /// The cursor should be rendered using a custom application-provided image.
+ ///
+ Custom = 1 << 7,
+}
\ No newline at end of file
diff --git a/sources/Input/Input/CustomCursor.cs b/sources/Input/Input/CustomCursor.cs
new file mode 100644
index 0000000000..b82b9d0337
--- /dev/null
+++ b/sources/Input/Input/CustomCursor.cs
@@ -0,0 +1,22 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a custom image for a mouse cursor.
+///
+public readonly ref struct CustomCursor
+{
+ ///
+ /// The number of pixels in the X axis.
+ ///
+ public int Width { get; init; }
+
+ ///
+ /// The number of pixels in the Y axis.
+ ///
+ public int Height { get; init; }
+
+ ///
+ /// The row-major 32-bit RGBA pixel data (i.e. 8 bits for each colour component).
+ ///
+ public ReadOnlySpan Data { get; init; } // Rgba32
+}
diff --git a/sources/Input/Input/DualReadOnlyList.cs b/sources/Input/Input/DualReadOnlyList.cs
new file mode 100644
index 0000000000..1fdf87c91a
--- /dev/null
+++ b/sources/Input/Input/DualReadOnlyList.cs
@@ -0,0 +1,48 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a list that has exactly two elements.
+///
+/// The element type.
+public readonly struct DualReadOnlyList(T left, T right) : IReadOnlyList
+{
+ ///
+ /// Creates a copy of the given list.
+ ///
+ /// The list.
+ public DualReadOnlyList(DualReadOnlyList other)
+ : this(other.Left, other.Right) { }
+
+ ///
+ /// The first/leftmost element.
+ ///
+ public readonly T Left = left;
+
+ ///
+ /// The second/rightmost element.
+ ///
+ public readonly T Right = right;
+
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ yield return Left;
+ yield return Right;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public int Count => 2;
+
+ ///
+ public T this[int index] =>
+ index switch
+ {
+ 0 => Left,
+ 1 => Right,
+ _ => throw new IndexOutOfRangeException(),
+ };
+}
diff --git a/sources/Input/Input/GamepadState.cs b/sources/Input/Input/GamepadState.cs
new file mode 100644
index 0000000000..9031830548
--- /dev/null
+++ b/sources/Input/Input/GamepadState.cs
@@ -0,0 +1,39 @@
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains user input received from an .
+///
+public class GamepadState(
+ ButtonReadOnlyList buttons,
+ DualReadOnlyList thumbsticks,
+ DualReadOnlyList triggers
+)
+{
+ ///
+ /// Clones the given state. This is useful for creating an immutable copy of state from a mutable one.
+ ///
+ /// The other state.
+ public GamepadState(GamepadState other)
+ : this(
+ new ButtonReadOnlyList(other.Buttons),
+ new DualReadOnlyList(other.Thumbsticks),
+ new DualReadOnlyList(other.Triggers)
+ ) { }
+
+ ///
+ /// Gets the gamepad button state denoting the buttons being pressed or depressed.
+ ///
+ public ButtonReadOnlyList Buttons { get; } = buttons;
+
+ ///
+ /// Gets the state of the twin sticks on the gamepad.
+ ///
+ public DualReadOnlyList Thumbsticks { get; internal set; } = thumbsticks;
+
+ ///
+ /// Gets the state of the triggers on the gamepad.
+ ///
+ public DualReadOnlyList Triggers { get; internal set; } = triggers;
+}
diff --git a/sources/Input/Input/GamepadThumbstickMoveEvent.cs b/sources/Input/Input/GamepadThumbstickMoveEvent.cs
new file mode 100644
index 0000000000..b2ffeef3a3
--- /dev/null
+++ b/sources/Input/Input/GamepadThumbstickMoveEvent.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics;
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to the movement of a thumbstick.
+///
+/// The gamepad on which the thumbstick resides.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+///
+/// The new position of the thumbstick, where each axis is between -1.0 and 1.0 .
+///
+/// The change in as a result of this event.
+public readonly record struct GamepadThumbstickMoveEvent(IGamepad Gamepad, long Timestamp, Vector2 Value, Vector2 Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/GamepadTriggerMoveEvent.cs b/sources/Input/Input/GamepadTriggerMoveEvent.cs
new file mode 100644
index 0000000000..0cbca61581
--- /dev/null
+++ b/sources/Input/Input/GamepadTriggerMoveEvent.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains information pertaining to the movement of a trigger.
+///
+/// The gamepad on which the trigger resides.
+///
+/// The timestamp (as retrieved from ) at which the event occurred.
+///
+/// The index of the trigger that has moved.
+///
+/// The new value of the trigger, between 0.0 (fully depressed) and 1.0 (fully pressed).
+///
+/// The change in as a result of this event.
+public readonly record struct GamepadTriggerMoveEvent(IGamepad Gamepad, long Timestamp, int Axis, float Value, float Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/Gamepads.cs b/sources/Input/Input/Gamepads.cs
new file mode 100644
index 0000000000..ce56d20079
--- /dev/null
+++ b/sources/Input/Input/Gamepads.cs
@@ -0,0 +1,43 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a collection of s from which input events can be received.
+///
+public sealed class Gamepads : InputContextDeviceList, IGamepadInputHandler
+{
+ internal Gamepads(InputContext ctx)
+ : base(ctx) { }
+
+ ///
+ /// Raised when state pertaining to a pushable button on the gamepad changes (e.g. button up, button down).
+ ///
+ public event Action>? ButtonChanged;
+
+ ///
+ /// Raised when a thumbstick on the gamepad moves.
+ ///
+ public event Action? ThumbstickMove;
+
+ ///
+ /// Raised when a trigger on the gamepad moves.
+ ///
+ public event Action? TriggerMove;
+
+ internal void HandleButtonChanged(ButtonChangedEvent @event) =>
+ ButtonChanged?.Invoke(@event);
+
+ void IButtonInputHandler.HandleButtonChanged(
+ ButtonChangedEvent @event
+ ) => HandleButtonChanged(@event);
+
+ internal void HandleThumbstickMove(GamepadThumbstickMoveEvent @event) =>
+ ThumbstickMove?.Invoke(@event);
+
+ void IGamepadInputHandler.HandleThumbstickMove(GamepadThumbstickMoveEvent @event) =>
+ HandleThumbstickMove(@event);
+
+ internal void HandleTriggerMove(GamepadTriggerMoveEvent @event) => TriggerMove?.Invoke(@event);
+
+ void IGamepadInputHandler.HandleTriggerMove(GamepadTriggerMoveEvent @event) =>
+ HandleTriggerMove(@event);
+}
diff --git a/sources/Input/Input/IButtonDevice.cs b/sources/Input/Input/IButtonDevice.cs
new file mode 100644
index 0000000000..70b88af9d3
--- /dev/null
+++ b/sources/Input/Input/IButtonDevice.cs
@@ -0,0 +1,17 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents an input device that has buttons.
+///
+/// The type of buttons the input device has.
+public interface IButtonDevice : IInputDevice
+ where T : unmanaged, Enum
+{
+ ///
+ /// Gets the current button state for this device.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ ButtonReadOnlyList State { get; }
+}
diff --git a/sources/Input/Input/IButtonInputHandler.cs b/sources/Input/Input/IButtonInputHandler.cs
new file mode 100644
index 0000000000..0d02d1d675
--- /dev/null
+++ b/sources/Input/Input/IButtonInputHandler.cs
@@ -0,0 +1,15 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives events.
+///
+/// The device's button type.
+public interface IButtonInputHandler : IInputHandler
+ where T : unmanaged, Enum
+{
+ ///
+ /// Called when a button's state changes (e.g. button down, button up).
+ ///
+ /// The event details.
+ void HandleButtonChanged(ButtonChangedEvent @event);
+}
diff --git a/sources/Input/Input/ICursorConfiguration.cs b/sources/Input/Input/ICursorConfiguration.cs
new file mode 100644
index 0000000000..0d0209d4e5
--- /dev/null
+++ b/sources/Input/Input/ICursorConfiguration.cs
@@ -0,0 +1,45 @@
+namespace Silk.NET.Input;
+
+///
+/// Configuration for the behaviour of a mouse cursor.
+///
+public interface ICursorConfiguration
+{
+ ///
+ /// Gets a bitmask denoting the supported values for .
+ ///
+ CursorModes SupportedModes { get; }
+
+ ///
+ /// Gets or sets the current cursor mode. Only one bit shall be set at a time.
+ ///
+ ///
+ /// Note that this property affects the in use, see the
+ /// documentation for more info.
+ ///
+ CursorModes Mode { get; set; }
+
+ ///
+ /// Gets a bitmask denoting the supported values for .
+ ///
+ CursorStyles SupportedStyles { get; }
+
+ ///
+ /// Gets or sets the current cursor style. Only one bit shall be set at a time.
+ /// shall use the provided.
+ ///
+ ///
+ /// When is used, the cursor ceases to exist anyway. As such, while the
+ /// property may not reflect this (as it is retained across changes to
+ /// and just ignored when is used),
+ /// can be implied as being when
+ /// is used.
+ ///
+ CursorStyles Style { get; set; }
+
+ ///
+ /// Gets or sets the current custom cursor image. This has no effect if is not
+ /// used, but the value is stored nonetheless for use when that is the case.
+ ///
+ CustomCursor Image { get; set; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IGamepad.cs b/sources/Input/Input/IGamepad.cs
new file mode 100644
index 0000000000..1dc37823b6
--- /dev/null
+++ b/sources/Input/Input/IGamepad.cs
@@ -0,0 +1,20 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a gamepad that follows a typical layout.
+///
+public interface IGamepad : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new GamepadState State { get; }
+ ButtonReadOnlyList IButtonDevice.State => State.Buttons;
+ ///
+ /// Gets a collection enumerating the vibration motors available to the application to enable haptics.
+ ///
+ IReadOnlyList VibrationMotors { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IGamepadInputHandler.cs b/sources/Input/Input/IGamepadInputHandler.cs
new file mode 100644
index 0000000000..b1bf488d8d
--- /dev/null
+++ b/sources/Input/Input/IGamepadInputHandler.cs
@@ -0,0 +1,19 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives input.
+///
+public interface IGamepadInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when one of the twin sticks moves.
+ ///
+ /// The event details.
+ void HandleThumbstickMove(GamepadThumbstickMoveEvent @event);
+
+ ///
+ /// Called when one of the two triggers moves.
+ ///
+ /// The event details.
+ void HandleTriggerMove(GamepadTriggerMoveEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IInputBackend.cs b/sources/Input/Input/IInputBackend.cs
new file mode 100644
index 0000000000..e67ce94d0f
--- /dev/null
+++ b/sources/Input/Input/IInputBackend.cs
@@ -0,0 +1,49 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents an input backend capable of receiving human input from Human Input Devices (HIDs).
+///
+///
+/// The onus is on the user to coordinate using this type across threads, as the input backend is not thread safe
+/// In addition, certain backends may have (unavoidable) restrictions on what thread can be called
+/// on - the user is responsible for respecting these threading rules as well.
+///
+public interface IInputBackend : IDisposable
+{
+ ///
+ /// Gets a rough human-readable description of the input backend. Its value is not intrinsically meaningful.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets a globally-unique integral identifier for this device.
+ ///
+ nint Id { get; }
+
+ ///
+ /// Get a list containing all the connected devices available from this input backend.
+ ///
+ ///
+ /// When a device is disconnected, its shall no longer function and will not be
+ /// enumerated by this list. When a device is connected, an with that physical device ID
+ /// shall be added to this list. In addition, upon connection any past objects previously
+ /// enumerated by this list on this instance shall also regain function if the device
+ /// being added to this list shares the same physical device ID as those previous instances. All such previous
+ /// instances shall be equatable to one another and to the instance added to this list.
+ /// An implementation is welcome to reuse old objects, but this is strictly implementation-defined. A device not
+ /// being present in the (checked using s
+ /// implementation) list is sufficient evidence that a device has been
+ /// disconnected.
+ ///
+ IReadOnlyList Devices { get; }
+
+ ///
+ /// Polls and updates the state of the objects connected using this backend, sending
+ /// input events to the given to reflect the human input received.
+ ///
+ ///
+ /// The value of the State properties on each device must not change until this method is called.
+ ///
+ /// The input handler.
+ void Update(IInputHandler? handler = null);
+}
diff --git a/sources/Input/Input/IInputDevice.cs b/sources/Input/Input/IInputDevice.cs
new file mode 100644
index 0000000000..a53a47d750
--- /dev/null
+++ b/sources/Input/Input/IInputDevice.cs
@@ -0,0 +1,36 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a connected Human Input Device (HID).
+///
+///
+/// All devices originate from a backend.
+///
+/// An object shall be equatable to any such object retrieved from the same backend where
+/// is equal.
+///
+/// objects must not store any managed state, and if there is a requirement for this in a
+/// future extension of this API then this must be defined in such a way that the state storage and lifetime is
+/// user-controlled. While objects are equatable based on s, if a physical
+/// device disconnects and reconnects the does not provide a guarantee that the same object
+/// will be returned (primarily because doing so would require the to keep track of every
+/// object it's ever created), rather a "compatible" one that acts identically to the original object. This is
+/// completely benign if the object is nothing but a wrapper to the backend anyway. If there is unmanaged state (e.g. a
+/// handle to a device that must be explicitly closed upon disconnection), then it is expected that even in the event of
+/// reconnection, old objects (e.g. created with a now-disposed handle) shall still work for the newly-reconnected
+/// device. A common way this could be implemented is storing the handles in the
+/// implementation instead in the form of a mapping of physical device IDs ( ) to those handles. This
+/// solves the object lifetime problem while also not adding undue complications to user code.
+///
+public interface IInputDevice : IEquatable
+{
+ ///
+ /// Gets a globally-unique integral identifier for this device.
+ ///
+ nint Id { get; }
+
+ ///
+ /// Gets a rough human-readable description of the input device. Its value is not intrinsically meaningful.
+ ///
+ string Name { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IInputHandler.cs b/sources/Input/Input/IInputHandler.cs
new file mode 100644
index 0000000000..3a7c7bbccc
--- /dev/null
+++ b/sources/Input/Input/IInputHandler.cs
@@ -0,0 +1,15 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a handler of human input. Implementations of this type will receive a method call for each distinctive
+/// HID event received in the order they were received, to the best of the backend's ability. All visible changes to
+/// device state correspond to a method call using this interface.
+///
+public interface IInputHandler
+{
+ ///
+ /// Called when an disconnects from the application.
+ ///
+ /// The event details.
+ void HandleDeviceConnectionChanged(ConnectionEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IJoystick.cs b/sources/Input/Input/IJoystick.cs
new file mode 100644
index 0000000000..df5bb9b3b3
--- /dev/null
+++ b/sources/Input/Input/IJoystick.cs
@@ -0,0 +1,16 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a joystick with axes, buttons, and hats.
+///
+public interface IJoystick : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new JoystickState State { get; }
+ ButtonReadOnlyList IButtonDevice.State => State.Buttons;
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IJoystickInputHandler.cs b/sources/Input/Input/IJoystickInputHandler.cs
new file mode 100644
index 0000000000..5dca7202d1
--- /dev/null
+++ b/sources/Input/Input/IJoystickInputHandler.cs
@@ -0,0 +1,19 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives input.
+///
+public interface IJoystickInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when an axis on the joystick moves.
+ ///
+ /// The event details.
+ void HandleAxisMove(JoystickAxisMoveEvent @event);
+
+ ///
+ /// Called when a hat on the joystick moves.
+ ///
+ /// The event details.
+ void HandleHatMove(JoystickHatMoveEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IKeyboard.cs b/sources/Input/Input/IKeyboard.cs
new file mode 100644
index 0000000000..5dcbf4896c
--- /dev/null
+++ b/sources/Input/Input/IKeyboard.cs
@@ -0,0 +1,51 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a keyboard device.
+///
+public interface IKeyboard : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new KeyboardState State { get; }
+
+ ButtonReadOnlyList IButtonDevice.State => State.Keys;
+
+ ///
+ /// Gets or sets the current text on the clipboard.
+ ///
+ string? ClipboardText { get; set; }
+
+ ///
+ /// Attempts to get a user-displayable string in the user's locale for the key at the physical position represented
+ /// by in the user's current keyboard layout.
+ ///
+ /// The physical key name. Consult documentation for more info.
+ /// The user-displayable name of the key.
+ /// Whether the name was successfully retrieved.
+ bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name);
+
+ ///
+ /// Begins recording keyboard input. Without / , there is no
+ /// guarantee that will be raised as this might require displaying
+ /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call
+ /// when you'd like to capture text input (e.g. in a text box), followed by
+ /// when you have completed collecting such input.
+ ///
+ void BeginInput();
+
+ ///
+ /// Concludes recording keyboard input. Without / , there is no
+ /// guarantee that will be raised as this might require displaying
+ /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call
+ /// when you'd like to capture text input (e.g. in a text box), followed by
+ /// when you have completed collecting such input.
+ ///
+ string? EndInput();
+}
diff --git a/sources/Input/Input/IKeyboardInputHandler.cs b/sources/Input/Input/IKeyboardInputHandler.cs
new file mode 100644
index 0000000000..6ca22ec632
--- /dev/null
+++ b/sources/Input/Input/IKeyboardInputHandler.cs
@@ -0,0 +1,23 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives events.
+///
+public interface IKeyboardInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when a key is pressed or depressed.
+ ///
+ /// The event details.
+ void HandleKeyChanged(KeyChangedEvent @event);
+
+ ///
+ /// Called when a character is typed.
+ ///
+ ///
+ /// Ensure you have called to start receiving text, after which events will be
+ /// sent for each character until is called.
+ ///
+ /// The event details.
+ void HandleKeyChar(KeyCharEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IMotor.cs b/sources/Input/Input/IMotor.cs
new file mode 100644
index 0000000000..f8875e7149
--- /dev/null
+++ b/sources/Input/Input/IMotor.cs
@@ -0,0 +1,13 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a vibration motor.
+///
+public interface IMotor
+{
+ ///
+ /// Gets or sets the speed at which the motor is operating, where 0.0 represents no vibration and 1.0
+ /// represents the maximum amount of vibration.
+ ///
+ float Speed { get; set; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IMouse.cs b/sources/Input/Input/IMouse.cs
new file mode 100644
index 0000000000..e71c8d7162
--- /dev/null
+++ b/sources/Input/Input/IMouse.cs
@@ -0,0 +1,34 @@
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a mouse - a type of pointer device.
+///
+public interface IMouse : IPointerDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new MouseState State { get; }
+
+ PointerState IPointerDevice.State => State;
+
+ ///
+ /// Gets the cursor configuration of the mouse.
+ ///
+ ///
+ /// This will determine which points shall lie on.
+ ///
+ ICursorConfiguration Cursor { get; }
+
+ ///
+ /// Attempts to set the position of the mouse.
+ ///
+ /// The position of the mouse in window coordinates.
+ /// Whether the requested position has been applied.
+ bool TrySetPosition(Vector2 position);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IMouseInputHandler.cs b/sources/Input/Input/IMouseInputHandler.cs
new file mode 100644
index 0000000000..71d02ad1c6
--- /dev/null
+++ b/sources/Input/Input/IMouseInputHandler.cs
@@ -0,0 +1,13 @@
+namespace Silk.NET.Input;
+
+///
+/// An that receives input from an .
+///
+public interface IMouseInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when the user scrolls using the scroll wheel.
+ ///
+ /// The event details.
+ void HandleScroll(MouseScrollEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IPointerDevice.cs b/sources/Input/Input/IPointerDevice.cs
new file mode 100644
index 0000000000..de4e803f28
--- /dev/null
+++ b/sources/Input/Input/IPointerDevice.cs
@@ -0,0 +1,20 @@
+namespace Silk.NET.Input;
+
+///
+/// Represents a device with which the user can point at a target.
+///
+public interface IPointerDevice : IButtonDevice
+{
+ ///
+ /// Gets the device state.
+ ///
+ ///
+ /// Only updated when is called.
+ ///
+ new PointerState State { get; }
+ ButtonReadOnlyList IButtonDevice.State => State.Buttons;
+ ///
+ /// Gets the targets at which the user can point with their pointer.
+ ///
+ IReadOnlyList Targets { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IPointerInputHandler.cs b/sources/Input/Input/IPointerInputHandler.cs
new file mode 100644
index 0000000000..f4bd67c0fc
--- /dev/null
+++ b/sources/Input/Input/IPointerInputHandler.cs
@@ -0,0 +1,26 @@
+namespace Silk.NET.Input;
+
+///
+/// An that also receives events.
+///
+public interface IPointerInputHandler : IButtonInputHandler
+{
+ ///
+ /// Called when the properties of a target at which the user can point using the pointer change. This includes the
+ /// addition and removal of targets.
+ ///
+ /// The event details.
+ void HandleTargetChanged(PointerTargetChangedEvent @event);
+
+ ///
+ /// Called when the user adds, removes, or changes a point at which they're pointing at a target.
+ ///
+ /// The event details.
+ void HandlePointChanged(PointChangedEvent @event);
+
+ ///
+ /// Called when the user changes the pressure with which they're gripping the pointer device.
+ ///
+ /// The event details.
+ void HandleGripChanged(PointerGripChangedEvent @event);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/IPointerTarget.cs b/sources/Input/Input/IPointerTarget.cs
new file mode 100644
index 0000000000..5bf3595bda
--- /dev/null
+++ b/sources/Input/Input/IPointerTarget.cs
@@ -0,0 +1,41 @@
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents a target at which the user can point using their pointer device.
+///
+public interface IPointerTarget
+{
+ ///
+ /// The boundary in which positions of points on this target shall fall. For ,
+ /// shall represent the lack of a lower bound on a particular axis. For
+ /// For , shall represent the lack of a lower bound
+ /// on a particular axis. 0 represents an unused axis that axis is 0 on both
+ /// and .
+ ///
+ Box3D Bounds { get; }
+
+ ///
+ /// Gets the number of points with which the given pointer is pointing at this target.
+ ///
+ /// The number of points.
+ ///
+ /// A single "logical" pointer device may have many points, and can optionally represent multiple physical pointers
+ /// as a single logical device - this is the case where a backend supports multiple mice to control an
+ /// cursor on its "raw mouse input" target, but combines these all to a single point on its "windowed" target. This
+ /// is also true for touch input - a touch screen is represented as a single touch device,
+ /// where each finger is its own point.
+ ///
+ int GetPointCount(IPointerDevice pointer);
+
+ ///
+ /// Gets a point with which the given pointer is pointing at this target.
+ ///
+ /// The pointer device.
+ ///
+ /// The index of the point, between 0 and the number sourced from .
+ ///
+ /// The point at the given index with which the given pointer device is pointing at the target.
+ TargetPoint GetPoint(IPointerDevice pointer, int point);
+}
\ No newline at end of file
diff --git a/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs
new file mode 100644
index 0000000000..bd515dc85e
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// ReSharper disable CheckNamespace
+
+using Silk.NET.Input.SDL3;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains extensions for creating input backends and contexts from s.
+///
+public static partial class InputWindowExtensions
+{
+ public static partial IInputBackend CreateInputBackend(this INativeWindow window)
+ {
+ if (!window.TryGetPlatformInfo(out SdlPlatformInfo info))
+ {
+ throw new ArgumentException(
+ "When using the Silk.NET.Input reference implementation, a native window compatible with that "
+ + "implementation (such as those sourced from Silk.NET.Windowing) must be used."
+ );
+ }
+
+ return new SdlInputBackend(info);
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs
new file mode 100644
index 0000000000..9105612b64
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerDevice.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input.SDL3;
+
+///
+/// A base class for SDL input devices that operate in terms of a window's or DWMs bounds.
+///
+/// The backend.
+internal abstract class SdlBoundedPointerDevice(SdlInputBackend backend)
+ : SdlDevice(backend),
+ IPointerDevice
+{
+ public abstract PointerState State { get; }
+
+ [field: MaybeNull]
+ public virtual IReadOnlyList Targets =>
+ field ??= [Backend.BoundedPointerTarget];
+
+ ///
+ /// Determines whether the should interpret
+ /// as being bounded points. For all devices supported by this backend, only one target is supported at a time
+ /// today.
+ ///
+ public virtual bool IsBounded => true;
+
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
+ public InputMarshal.ListOwner BoundedPoints =>
+ field.List.Data is null ? field = InputMarshal.CreateList() : field;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs
new file mode 100644
index 0000000000..f7aee7b94c
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlBoundedPointerTarget.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+using Silk.NET.Maths;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlBoundedPointerTarget(SdlInputBackend backend) : IPointerTarget
+{
+ internal SdlInputBackend Backend { get; } = backend;
+ private Box2D Bounds2D { get; set; }
+
+ public Box3D Bounds =>
+ new(new Vector3D(Bounds2D.Min, 0), new Vector3D(Bounds2D.Max, 1));
+
+ public static Box2D CalculateBounds(ISdl sdl)
+ {
+ var minX = float.PositiveInfinity;
+ var minY = float.PositiveInfinity;
+ var maxX = float.NegativeInfinity;
+ var maxY = float.NegativeInfinity;
+ var displayCount = 0;
+ var displays = sdl.GetDisplays(displayCount.AsRef());
+ if (displays == nullptr)
+ {
+ // Looks like we can't support windowed mouse input.
+ sdl.ClearError();
+ return default;
+ }
+
+ if (displayCount == 0) // ???
+ {
+ sdl.Free((Ref)displays);
+ return default;
+ }
+
+ for (var i = 0; i < displayCount; i++)
+ {
+ Rect rect = default;
+ if (!sdl.GetDisplayBounds(displays[(nuint)i], rect.AsRef()))
+ {
+ return default;
+ }
+
+ minX = float.Min(minX, rect.X);
+ minY = float.Min(minY, rect.Y);
+ maxX = float.Max(maxX, rect.X + rect.W);
+ maxY = float.Max(maxY, rect.Y + rect.H);
+ }
+
+ sdl.Free((Ref)displays);
+ if (minX <= maxX && minY <= maxY)
+ {
+ return new Box2D(minX, minY, maxX, maxY);
+ }
+
+ return default;
+ }
+
+ public int GetPointCount(IPointerDevice pointer)
+ {
+ if (pointer is not SdlBoundedPointerDevice { IsBounded: true } device)
+ {
+ return 0;
+ }
+
+ if (device.Backend == Backend)
+ {
+ return Bounds != default ? device.BoundedPoints.List.Count : 0;
+ }
+
+ return device.Backend.BoundedPointerTarget.GetPointCount(pointer);
+ }
+
+ public TargetPoint GetPoint(IPointerDevice pointer, int point)
+ {
+ if (
+ pointer is not SdlBoundedPointerDevice { IsBounded: true } device
+ || point < 0
+ || point >= device.BoundedPoints.List.Count
+ )
+ {
+ return default;
+ }
+
+ if (device.Backend != Backend)
+ {
+ return device.Backend.BoundedPointerTarget.GetPoint(pointer, point);
+ }
+
+ return Bounds != default ? device.BoundedPoints.List[point] : default;
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlDevice.cs b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs
new file mode 100644
index 0000000000..9980e32b84
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlDevice.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3;
+
+internal abstract class SdlDevice(SdlInputBackend backend) : IInputDevice
+{
+ public bool Equals(IInputDevice? other) =>
+ other?.GetType() == GetType()
+ && other.Id == Id
+ && other is SdlBoundedPointerDevice dev
+ && dev.Backend.Sdl == Backend.Sdl;
+
+ public abstract IntPtr Id { get; }
+ public abstract string Name { get; }
+ public SdlInputBackend Backend { get; } = backend;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs
new file mode 100644
index 0000000000..d24b71a89d
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs
@@ -0,0 +1,125 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlGamepad : SdlDevice, IGamepad, IDisposable
+{
+ private readonly GamepadHandle _gamepad;
+
+ private static JoystickButton? GetSilkButton(GamepadButton btn) =>
+ btn switch
+ {
+ GamepadButton.South => JoystickButton.ButtonDown,
+ GamepadButton.East => JoystickButton.ButtonRight,
+ GamepadButton.West => JoystickButton.ButtonLeft,
+ GamepadButton.North => JoystickButton.ButtonUp,
+ GamepadButton.Back => JoystickButton.Back,
+ GamepadButton.Guide => JoystickButton.Home,
+ GamepadButton.Start => JoystickButton.Start,
+ GamepadButton.LeftStick => JoystickButton.LeftStick,
+ GamepadButton.RightStick => JoystickButton.RightStick,
+ GamepadButton.LeftShoulder => JoystickButton.LeftBumper,
+ GamepadButton.RightShoulder => JoystickButton.RightBumper,
+ GamepadButton.DpadUp => JoystickButton.DPadUp,
+ GamepadButton.DpadDown => JoystickButton.DPadDown,
+ GamepadButton.DpadLeft => JoystickButton.DPadLeft,
+ GamepadButton.DpadRight => JoystickButton.DPadRight,
+ // TODO not exposed today
+ _ => null,
+ };
+
+ public SdlGamepad(SdlInputBackend backend, uint joystickId)
+ : base(backend)
+ {
+ _gamepad = backend.Sdl.OpenGamepad(joystickId);
+ if (_gamepad == nullptr)
+ {
+ backend.Sdl.ThrowError();
+ }
+
+ var buttons = InputMarshal.CreateList>();
+ for (var i = 0; i < (int)GamepadButton.Count; i++)
+ {
+ if (GetSilkButton((GamepadButton)i) is not { } btn)
+ {
+ continue;
+ }
+
+ var isDown = backend.Sdl.GetGamepadButton(_gamepad, (GamepadButton)i);
+ InputMarshal.SetButtonState(
+ buttons,
+ new Button(btn, isDown, isDown ? 1 : 0),
+ true
+ );
+ }
+
+ // For thumbsticks, the state is a value ranging from -32768 (up/left) to 32767 (down/right).
+ // Triggers range from 0 when released to 32767 when fully pressed, and never return a negative value. Note that
+ // this differs from the value reported by the lower-level SDL_GetJoystickAxis(), which normally uses the full
+ // range.
+ var triggers = new DualReadOnlyList(
+ (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.LeftTrigger) / short.MaxValue,
+ (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.RightTrigger) / short.MaxValue
+ );
+ var thumbsticks = new DualReadOnlyList(
+ new Vector2(
+ (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Leftx) / short.MaxValue,
+ (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Lefty) / short.MaxValue
+ ),
+ new Vector2(
+ (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Rightx) / short.MaxValue,
+ (float)backend.Sdl.GetGamepadAxis(_gamepad, GamepadAxis.Righty) / short.MaxValue
+ )
+ );
+ State = new GamepadState(buttons.List.AsButtonList(), thumbsticks, triggers);
+ }
+
+ // TODO this is not spec compliant, we need to use a physical device ID
+ public override unsafe nint Id => (nint)_gamepad.Handle;
+
+ public override string Name => Backend.Sdl.GetGamepadName(_gamepad).ReadToString();
+
+ public GamepadState State { get; }
+
+ // TODO this entire API needs to be redesigned as right now this is literally only ever going to be useful if it's
+ // just left or right. The original intention was that this would be useful for things like 3D haptics, but what did
+ // 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.
+ // For now, this has the same implementation as it always has.
+ public IReadOnlyList VibrationMotors =>
+ _motors ??= [new SdlMotor(this, 0), new SdlMotor(this, 1)];
+
+ private IMotor[]? _motors;
+ private ushort[]? _motorFrequencies;
+
+ internal ushort GetRumble(int motor) => (_motorFrequencies ??= [0, 0])[motor];
+
+ internal void SetRumble(int motor, ushort value)
+ {
+ (_motorFrequencies ??= [0, 0])[motor] = value;
+ if (
+ !Backend.Sdl.RumbleGamepad(
+ _gamepad,
+ _motorFrequencies[0],
+ _motorFrequencies[1],
+ uint.MaxValue
+ )
+ )
+ {
+ Backend.Sdl.ThrowError();
+ }
+ }
+
+ private void ReleaseUnmanagedResources() => Backend.Sdl.CloseGamepad(_gamepad);
+
+ public void Dispose()
+ {
+ ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ~SdlGamepad() => ReleaseUnmanagedResources();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs
new file mode 100644
index 0000000000..78ee265472
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs
@@ -0,0 +1,358 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using Silk.NET.Maths;
+using Silk.NET.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlInputBackend : IInputBackend, ICursorConfiguration
+{
+ private static readonly double _ticksPerNanosecond = Stopwatch.Frequency / 10e9d;
+
+ private bool _pumped;
+ private long _epoch;
+ private List _devices = [];
+ private List _eventQueue = [];
+ private WindowHandle _focusedWindow;
+ private ISdl _sdl;
+
+ public unsafe SdlInputBackend(SdlPlatformInfo info)
+ {
+ ArgumentNullException.ThrowIfNull(info.Sdl);
+ ArgumentNullException.ThrowIfNull(info.Window.Handle);
+ var ptr = new EventFilter(OnEvent);
+ _sdl = info.Sdl;
+ _focusedWindow = info.Window;
+ // TODO overload resolution priority?
+ if (!Sdl.AddEventWatch(ptr, (Ref)nullptr))
+ {
+ Sdl.ThrowError();
+ }
+
+ Id = (nint)ptr.Handle;
+
+ // The epoch deals in nanoseconds, so we take multiple measurements for the most accurate timestamps.
+ const byte epochMeasurements = 3;
+ var epoch = 0L;
+ for (byte i = 0; i < epochMeasurements; i++)
+ {
+ // We know the ticks per nanosecond, so to get the epoch timestamp we multiply the TicksNS by the ticks per
+ // nanosecond to get the number of ticks relative to SDL's epoch, and then subtract that from the timestamp
+ // now to get the timestamp of SDL's epoch. From there, when we receive an event we can just report the
+ // timestamp as _epoch + (timestamp * _ticksPerNanosecond).
+ var nowTimestamp = Stopwatch.GetTimestamp();
+ var nowTicks = Sdl.GetTicksNS();
+ epoch += unchecked(nowTimestamp - (long)(nowTicks * _ticksPerNanosecond));
+ }
+
+ _epoch = epoch / epochMeasurements;
+
+ // ===============================================================================================
+ // === If we ever need to share common state across window-specific "backends", use the below: ===
+ // ===============================================================================================
+ // // Get the root surface - our windowing backend assumes there is only one root surface. If this is not the
+ // // case then this is undefined behaviour.
+ // var rootSurface = info.Window;
+ // var parent = rootSurface;
+ // while ((parent = Sdl.GetWindowParent(rootSurface)) != nullptr)
+ // {
+ // rootSurface = parent;
+ // }
+ // // Get the surface properties.
+ // var props = Sdl.GetWindowProperties(rootSurface);
+ // if (props == 0)
+ // {
+ // Sdl.ThrowError();
+ // }
+ // // Get or create the root object.
+ // Ref pname = "org.dotnetfoundation.silkdotnet.inputroot";
+ // var root = (nint)Sdl.GetPointerProperty(props, pname, nullptr);
+ // if (root != 0)
+ // {
+ // Root =
+ // GCHandle.FromIntPtr(root).Target as SdlBackendRoot
+ // ?? throw new InvalidOperationException(
+ // "The global input data for this ancestry of SDL windows was not in an expected format."
+ // );
+ // }
+ // else
+ // {
+ // Root = new SdlBackendRoot();
+ // var newHandle = GCHandle.Alloc(Root);
+ // if (
+ // Sdl.SetPointerPropertyWithCleanup(
+ // props,
+ // pname,
+ // (Ptr)GCHandle.ToIntPtr(newHandle),
+ // new CleanupPropertyCallback(&CleanupRoot),
+ // nullptr
+ // )
+ // )
+ // {
+ // return;
+ // }
+ // newHandle.Free();
+ // Sdl.ThrowError();
+ // }
+ // // Register ourselves with the root.
+ // Root.Backends.Add(this, null);
+ // Id = (nint)Root.EventFilter.Handle + Root.Backends.Count() - 1;
+ }
+
+ // [UnmanagedCallersOnly]
+ // private static unsafe void CleanupRoot(void* _, void* value)
+ // {
+ // var gch = GCHandle.FromIntPtr((nint)value);
+ // (gch.Target as SdlBackendRoot)?.Dispose();
+ // gch.Free();
+ // }
+ // public SdlBackendRoot Root { get; }
+
+ // NOTE: Be careful where these are used!
+ public SdlPlatformInfo Info { get; }
+
+ [field: MaybeNull]
+ public SdlBoundedPointerTarget BoundedPointerTarget =>
+ field ??= new SdlBoundedPointerTarget(this);
+
+ [field: MaybeNull]
+ public SdlUnboundedPointerTarget UnboundedPointerTarget =>
+ field ??= new SdlUnboundedPointerTarget(this);
+
+ public ISdl Sdl => Info.Sdl ?? SDL.Sdl.Instance;
+
+ public string Name =>
+ $"Silk.NET.Input Reference Implementation using SDL3 ({Sdl.GetPlatform().ReadToString()})";
+
+ public nint Id { get; }
+
+ public IReadOnlyList Devices => _devices;
+
+ // TODO we can't query support for these modes, but should we try-it-and-see to be accurate?
+ public CursorModes SupportedModes =>
+ CursorModes.Normal | CursorModes.Confined | CursorModes.Unbounded;
+
+ // TODO if you're using one input context for all windows, there is no way to specify a window for grabbed cursor mode
+
+ public CursorModes Mode
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public CursorStyles SupportedStyles => throw new NotImplementedException();
+
+ public CursorStyles Style
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public CustomCursor Image
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ // This is complicated, as the input proposal mandates that nothing happens until Update is called (so the events
+ // can be received on the given actor) but to also track logical events that happen between calls (i.e. from a
+ // timestamp perspective). Compound this with the fact that the user might do something silly like make multiple
+ // input backends (which is feasible for multiple windows I guess), or not be running anything other than input
+ // (having obviously created a window beforehand but not actually polling events I guess)
+ public void Update(IInputHandler? handler = null)
+ {
+ if (!_pumped)
+ {
+ Sdl.PumpEvents();
+ }
+
+ _pumped = false;
+ throw new NotImplementedException();
+ }
+
+ private enum QueuedEventType : byte
+ {
+ ///
+ /// The mouse has exited the window and the shared point should be marked inactive until proven otherwise by
+ /// further mouse motion (indicating it has entered another window).
+ ///
+ ///
+ /// We do not track the mouse enter events as this would cause us to fire twice for a mouse entering a window:
+ /// once for the entering, and once for new position.
+ ///
+ MouseExitedWindow,
+
+ ///
+ /// The display bounds have been changed, meaning that 's
+ /// will have changed.
+ ///
+ BoundedPointerTargetUpdate,
+ }
+
+ private readonly record struct QueuedEvent(
+ QueuedEventType Type,
+ ulong Timestamp,
+ Vector2 Vector0 = default,
+ Vector2 Vector1 = default
+ );
+
+ private ulong GetTimestamp(ref readonly Event @event) =>
+ unchecked((ulong)(_epoch + (@event.Common.Timestamp * _ticksPerNanosecond)));
+
+ private unsafe byte OnEvent(void* arg0, Event* arg1)
+ {
+ _pumped = true;
+ // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
+ switch ((EventType)arg1->Common.Type)
+ {
+ case EventType.DisplayOrientation:
+ case EventType.DisplayAdded:
+ case EventType.DisplayRemoved:
+ case EventType.DisplayMoved:
+ case EventType.DisplayDesktopModeChanged:
+ case EventType.DisplayCurrentModeChanged:
+ case EventType.DisplayContentScaleChanged:
+ {
+ var bounds = SdlBoundedPointerTarget.CalculateBounds(Sdl);
+ _eventQueue.Add(
+ new QueuedEvent(
+ QueuedEventType.BoundedPointerTargetUpdate,
+ GetTimestamp(ref *arg1),
+ bounds.Min.ToSystem(),
+ bounds.Max.ToSystem()
+ )
+ );
+ break;
+ }
+ case EventType.WindowMouseLeave:
+ {
+ _eventQueue.Add(
+ new QueuedEvent(QueuedEventType.MouseExitedWindow, GetTimestamp(ref *arg1))
+ );
+ break;
+ }
+ case EventType.KeyDown:
+ break;
+ case EventType.KeyUp:
+ break;
+ case EventType.TextEditing:
+ break;
+ case EventType.TextInput:
+ break;
+ case EventType.KeymapChanged:
+ break;
+ case EventType.KeyboardAdded:
+ break;
+ case EventType.KeyboardRemoved:
+ break;
+ case EventType.TextEditingCandidates:
+ break;
+ case EventType.MouseMotion:
+ break;
+ case EventType.MouseButtonDown:
+ break;
+ case EventType.MouseButtonUp:
+ break;
+ case EventType.MouseWheel:
+ break;
+ case EventType.MouseAdded:
+ break;
+ case EventType.MouseRemoved:
+ break;
+ case EventType.JoystickAxisMotion:
+ break;
+ case EventType.JoystickBallMotion:
+ break;
+ case EventType.JoystickHatMotion:
+ break;
+ case EventType.JoystickButtonDown:
+ break;
+ case EventType.JoystickButtonUp:
+ break;
+ case EventType.JoystickAdded:
+ break;
+ case EventType.JoystickRemoved:
+ break;
+ case EventType.JoystickBatteryUpdated:
+ break;
+ case EventType.JoystickUpdateComplete:
+ break;
+ case EventType.GamepadAxisMotion:
+ break;
+ case EventType.GamepadButtonDown:
+ break;
+ case EventType.GamepadButtonUp:
+ break;
+ case EventType.GamepadAdded:
+ break;
+ case EventType.GamepadRemoved:
+ break;
+ case EventType.GamepadRemapped:
+ break;
+ case EventType.GamepadTouchpadDown:
+ break;
+ case EventType.GamepadTouchpadMotion:
+ break;
+ case EventType.GamepadTouchpadUp:
+ break;
+ case EventType.GamepadSensorUpdate:
+ break;
+ case EventType.GamepadUpdateComplete:
+ break;
+ case EventType.GamepadSteamHandleUpdated:
+ break;
+ case EventType.FingerDown:
+ break;
+ case EventType.FingerUp:
+ break;
+ case EventType.FingerMotion:
+ break;
+ case EventType.FingerCanceled:
+ break;
+ case EventType.ClipboardUpdate:
+ break;
+ case EventType.SensorUpdate:
+ break;
+ case EventType.PenProximityIn:
+ break;
+ case EventType.PenProximityOut:
+ break;
+ case EventType.PenDown:
+ break;
+ case EventType.PenUp:
+ break;
+ case EventType.PenButtonDown:
+ break;
+ case EventType.PenButtonUp:
+ break;
+ case EventType.PenMotion:
+ break;
+ case EventType.PenAxis:
+ break;
+ }
+
+ return 1;
+ }
+
+ private unsafe void ReleaseUnmanagedResources()
+ {
+ Sdl.RemoveEventWatch(
+ new EventFilter((delegate* unmanaged)(void*)Id),
+ nullptr
+ );
+ SilkMarshal.Free((Ptr)Id);
+ }
+
+ public void Dispose()
+ {
+ ReleaseUnmanagedResources();
+ GC.SuppressFinalize(this);
+ }
+
+ ~SdlInputBackend() => ReleaseUnmanagedResources();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs
new file mode 100644
index 0000000000..0dd57a7c44
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs
@@ -0,0 +1,16 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlJoystick : SdlDevice, IJoystick
+{
+ public SdlJoystick(SdlInputBackend backend, uint joystick)
+ : base(backend) { }
+
+ public override IntPtr Id => throw new NotImplementedException();
+
+ public override string Name => throw new NotImplementedException();
+
+ public JoystickState State => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs
new file mode 100644
index 0000000000..eb8ab4b208
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlKeyboard : SdlDevice, IKeyboard
+{
+ public SdlKeyboard(SdlInputBackend backend)
+ : base(backend) { }
+
+ public override nint Id => throw new NotImplementedException();
+
+ public override string Name => throw new NotImplementedException();
+
+ public KeyboardState State => throw new NotImplementedException();
+
+ public string? ClipboardText
+ {
+ get => throw new NotImplementedException();
+ set => throw new NotImplementedException();
+ }
+
+ public bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name) =>
+ throw new NotImplementedException();
+
+ public void BeginInput() => throw new NotImplementedException();
+
+ public string? EndInput() => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlMotor.cs b/sources/Input/Input/Implementations/SDL3/SdlMotor.cs
new file mode 100644
index 0000000000..77a508bf6e
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlMotor.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlMotor(SdlGamepad gamepad, int freqIdx) : IMotor
+{
+ public float Speed
+ {
+ get => (float)gamepad.GetRumble(freqIdx) / ushort.MaxValue;
+ set => gamepad.SetRumble(freqIdx, (ushort)(value * ushort.MaxValue));
+ }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/SdlPen.cs
new file mode 100644
index 0000000000..3acaa39c24
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlPen.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlPen : SdlBoundedPointerDevice
+{
+ private uint _penId;
+
+ public SdlPen(SdlInputBackend backend, uint pen)
+ : base(backend)
+ {
+ _penId = pen;
+ }
+
+ public override IntPtr Id => HashCode.Combine(Backend.Id, _penId);
+
+ public override string Name => throw new NotImplementedException();
+
+ public override PointerState State => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs
new file mode 100644
index 0000000000..e40c95cc11
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlSharedMouse.cs
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlSharedMouse : SdlBoundedPointerDevice, IMouse
+{
+ private readonly MouseState _state;
+
+ public SdlSharedMouse(SdlInputBackend backend)
+ : base(backend)
+ {
+ var buttons = InputMarshal.CreateList>(32);
+ var points = InputMarshal.CreateList(1);
+ _state = new MouseState(buttons.List.AsButtonList(), points.List, Vector2.Zero);
+ float x = 0,
+ y = 0;
+ var buttonMask = backend.Sdl.GetMouseState(x.AsRef(), y.AsRef());
+ for (var i = 0; i < 32; i++)
+ {
+ InputMarshal.SetButtonState(
+ buttons,
+ new Button(
+ i switch
+ {
+ 1 => PointerButton.MiddleButton,
+ 2 => PointerButton.Secondary,
+ _ => (PointerButton)(i + 1),
+ },
+ (buttonMask & (1 << i)) != 0,
+ 0
+ ),
+ true
+ );
+ }
+
+ var pos = new Vector2(x, y);
+ var bounds = backend.BoundedPointerTarget.Bounds;
+ var min = new Vector2(bounds.Min.X, bounds.Min.Y);
+ var max = new Vector2(bounds.Max.X, bounds.Max.Y);
+ points
+ .GetUnderlyingList()!
+ .Add(
+ new TargetPoint(
+ 0,
+ TargetPointFlags.PointingAtTarget,
+ new Vector3(pos - min, 0),
+ new Vector3((pos - min) / (max - min), 0),
+ default,
+ 1.0f,
+ backend.BoundedPointerTarget
+ )
+ );
+ }
+
+ public override IntPtr Id => HashCode.Combine(Backend.Id);
+
+ public override string Name => $"{Backend.Name}: Shared/Global Mouse";
+
+ MouseState IMouse.State => _state;
+
+ public ICursorConfiguration Cursor => Backend;
+
+ public bool TrySetPosition(Vector2 position)
+ {
+ if (Backend.Sdl.WarpMouseGlobal(position.X, position.Y))
+ {
+ return true;
+ }
+
+ Backend.Sdl.ClearError();
+ return false;
+ }
+
+ public override PointerState State => _state;
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs b/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs
new file mode 100644
index 0000000000..7219f1a1d9
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlTouchScreen.cs
@@ -0,0 +1,17 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlTouchScreen : IPointerDevice
+{
+ public bool Equals(IInputDevice? other) => throw new NotImplementedException();
+
+ public IntPtr Id => throw new NotImplementedException();
+
+ public string Name => throw new NotImplementedException();
+
+ public PointerState State => throw new NotImplementedException();
+
+ public IReadOnlyList Targets => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs
new file mode 100644
index 0000000000..6d4b6d58d2
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlUnboundedMouse.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Numerics;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlUnboundedMouse : SdlDevice, IMouse
+{
+ public SdlUnboundedMouse(SdlInputBackend backend, uint mouseId)
+ : base(backend) { }
+
+ public override IntPtr Id => throw new NotImplementedException();
+
+ public override string Name => throw new NotImplementedException();
+
+ public MouseState State => throw new NotImplementedException();
+
+ public ICursorConfiguration Cursor => Backend;
+
+ public bool TrySetPosition(Vector2 position) => throw new NotImplementedException();
+
+ public IReadOnlyList Targets => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs b/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs
new file mode 100644
index 0000000000..797453aef1
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlUnboundedPointerTarget.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlUnboundedPointerTarget(SdlInputBackend backend) : IPointerTarget
+{
+ private static readonly Box3D _bounds = new(
+ float.MinValue,
+ float.MinValue,
+ float.MinValue,
+ float.MaxValue,
+ float.MaxValue,
+ float.MaxValue
+ );
+
+ public Box3D Bounds => _bounds;
+
+ public int GetPointCount(IPointerDevice pointer)
+ {
+ if (pointer is not SdlUnboundedMouse mouse)
+ {
+ return 0;
+ }
+
+ if (mouse.Backend != backend)
+ {
+ return mouse.Backend.UnboundedPointerTarget.GetPointCount(pointer);
+ }
+
+ return (mouse.Backend.Mode & CursorModes.Unbounded) != 0 ? 1 : 0;
+ }
+
+ public TargetPoint GetPoint(IPointerDevice pointer, int point) =>
+ throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/InputContext.cs b/sources/Input/Input/InputContext.cs
new file mode 100644
index 0000000000..402735e702
--- /dev/null
+++ b/sources/Input/Input/InputContext.cs
@@ -0,0 +1,241 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+///
+/// Represents an "input context" containing multiple s from which
+/// s, their state, and their events are aggregated and laid-out in a user-friendly fashion.
+///
+///
+/// The onus is on the user to coordinate using this type across threads, as the input backend is not thread safe
+/// In addition, certain backends may have (unavoidable) restrictions on what thread can be called
+/// on - the user is responsible for respecting these threading rules as well.
+///
+public class InputContext
+ : IJoystickInputHandler,
+ IGamepadInputHandler,
+ IMouseInputHandler,
+ IPointerInputHandler,
+ IKeyboardInputHandler,
+ IList
+{
+ // These are lazy-initialized as they contain their own device lists in addition to the device list stored here and
+ // the device lists stored in each of the backends. You could argue having this many duplicated lists is inefficient
+ // and you'd be absolutely right, but realistically: how many devices will the average user have connected to their
+ // PC? If you're worried about your game's memory consumption, you're probably not looking at the small lists that
+ // input allocates... This way we can also provide sane/consistent indices.
+ private Pointers? _pointers;
+ private Keyboards? _keyboards;
+ private Gamepads? _gamepads;
+ private Joysticks? _joysticks;
+ private List _backends = [];
+ private List? _devices;
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Pointers Pointers => _pointers ??= new Pointers(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Keyboards Keyboards => _keyboards ??= new Keyboards(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Gamepads Gamepads => _gamepads ??= new Gamepads(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public Joysticks Joysticks => _joysticks ??= new Joysticks(this);
+
+ ///
+ /// Gets the s enumerated by the s attached to this context.
+ ///
+ public IReadOnlyList Devices
+ {
+ get
+ {
+ if (_devices is not null)
+ {
+ return _devices;
+ }
+
+ foreach (var backend in Backends)
+ {
+ _devices ??= new List(backend.Devices.Count);
+ _devices.AddRange(backend.Devices);
+ }
+
+ return _devices ??= [];
+ }
+ }
+
+ ///
+ /// Gets a list denoting the attached to this context.
+ ///
+ public IList Backends => this;
+
+ ///
+ /// Raised when a device is added or removed from the list of connected .
+ ///
+ public event Action? ConnectionChanged;
+
+ ///
+ /// Polls and updates the state of the objects connected to each
+ /// attached to this context, raising appropriate events for each state change.
+ ///
+ ///
+ /// This calls for each attached to this context.
+ ///
+ public void Update()
+ {
+ foreach (var backend in Backends)
+ {
+ backend.Update(this);
+ }
+
+ _pointers?.HandleUpdate();
+ }
+
+ private void HandleBackendRemoval(IInputBackend backend)
+ {
+ foreach (var device in backend.Devices)
+ {
+ HandleDeviceConnectionChanged(new ConnectionEvent(device, 0, false));
+ }
+ }
+
+ private void HandleBackendAddition(IInputBackend backend)
+ {
+ foreach (var device in backend.Devices)
+ {
+ HandleDeviceConnectionChanged(new ConnectionEvent(device, 0, true));
+ }
+ }
+
+ private void HandleDeviceConnectionChanged(ConnectionEvent e)
+ {
+ _pointers?.HandleDeviceConnectionChanged(e);
+ _joysticks?.HandleDeviceConnectionChanged(e);
+ _gamepads?.HandleDeviceConnectionChanged(e);
+ _keyboards?.HandleDeviceConnectionChanged(e);
+ if (_devices is null)
+ {
+ return;
+ }
+
+ if (e.IsConnected)
+ {
+ _devices?.Add(e.Device);
+ }
+ else
+ {
+ _devices?.Remove(e.Device);
+ }
+ }
+
+ void IButtonInputHandler.HandleButtonChanged(
+ ButtonChangedEvent @event
+ ) => _joysticks?.HandleButtonChanged(@event);
+
+ void IJoystickInputHandler.HandleAxisMove(JoystickAxisMoveEvent @event) =>
+ _joysticks?.HandleAxisMove(@event);
+
+ void IJoystickInputHandler.HandleHatMove(JoystickHatMoveEvent @event) =>
+ _joysticks?.HandleHatMove(@event);
+
+ void IGamepadInputHandler.HandleThumbstickMove(GamepadThumbstickMoveEvent @event) =>
+ _gamepads?.HandleThumbstickMove(@event);
+
+ void IGamepadInputHandler.HandleTriggerMove(GamepadTriggerMoveEvent @event) =>
+ _gamepads?.HandleTriggerMove(@event);
+
+ void IButtonInputHandler.HandleButtonChanged(
+ ButtonChangedEvent @event
+ ) => _pointers?.HandleButtonChanged(@event);
+
+ void IMouseInputHandler.HandleScroll(MouseScrollEvent @event) =>
+ _pointers?.HandleScroll(@event);
+
+ void IPointerInputHandler.HandleTargetChanged(PointerTargetChangedEvent @event) =>
+ _pointers?.HandleTargetChanged(@event);
+
+ void IPointerInputHandler.HandlePointChanged(PointChangedEvent @event) =>
+ _pointers?.HandlePointChanged(@event);
+
+ void IPointerInputHandler.HandleGripChanged(PointerGripChangedEvent @event) =>
+ _pointers?.HandleGripChanged(@event);
+
+ void IButtonInputHandler.HandleButtonChanged(ButtonChangedEvent @event) =>
+ _keyboards?.HandleButtonChanged(@event);
+
+ void IKeyboardInputHandler.HandleKeyChanged(KeyChangedEvent @event) =>
+ _keyboards?.HandleKeyChanged(@event);
+
+ void IKeyboardInputHandler.HandleKeyChar(KeyCharEvent @event) =>
+ _keyboards?.HandleKeyChar(@event);
+
+ void IInputHandler.HandleDeviceConnectionChanged(ConnectionEvent @event)
+ {
+ HandleDeviceConnectionChanged(@event);
+ ConnectionChanged?.Invoke(@event);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() =>
+ _backends.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => _backends.GetEnumerator();
+
+ void ICollection.Add(IInputBackend item)
+ {
+ HandleBackendAddition(item);
+ _backends.Add(item);
+ }
+
+ void ICollection.Clear()
+ {
+ foreach (var backend in Backends)
+ {
+ HandleBackendRemoval(backend);
+ }
+ }
+
+ bool ICollection.Contains(IInputBackend item) => _backends.Contains(item);
+
+ void ICollection.CopyTo(IInputBackend[] array, int arrayIndex) =>
+ _backends.CopyTo(array, arrayIndex);
+
+ bool ICollection.Remove(IInputBackend item)
+ {
+ HandleBackendRemoval(item);
+ return _backends.Remove(item);
+ }
+
+ int ICollection.Count => _backends.Count;
+
+ bool ICollection.IsReadOnly => false;
+
+ int IList.IndexOf(IInputBackend item) => _backends.IndexOf(item);
+
+ void IList.Insert(int index, IInputBackend item)
+ {
+ HandleBackendAddition(item);
+ _backends.Insert(index, item);
+ }
+
+ void IList.RemoveAt(int index)
+ {
+ var backend = _backends[index];
+ HandleBackendRemoval(backend);
+ _backends.RemoveAt(index);
+ }
+
+ IInputBackend IList.this[int index]
+ {
+ get => _backends[index];
+ set => _backends[index] = value;
+ }
+}
diff --git a/sources/Input/Input/InputContextDeviceList.cs b/sources/Input/Input/InputContextDeviceList.cs
new file mode 100644
index 0000000000..1ae312526b
--- /dev/null
+++ b/sources/Input/Input/InputContextDeviceList.cs
@@ -0,0 +1,61 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input;
+
+///
+/// An internal class that represents a list of that are assignable to
+/// . The backing list is lazily initialized.
+///
+/// The device type.
+///
+/// This type is not intended for public consumption and has no API/ABI stability guarantees.
+///
+[Experimental(
+ "ST0005",
+ UrlFormat = "https://dotnet.github.io/Silk.NET/docs/v3/silk.net/diagnostics/{0}"
+)]
+public abstract class InputContextDeviceList : IReadOnlyList, IInputHandler
+{
+ private readonly InputContext _ctx;
+ private List? _list;
+
+ internal InputContextDeviceList(InputContext ctx) => _ctx = ctx;
+
+ private List List => _list ??= _ctx.Devices.OfType().ToList();
+
+ ///
+ public IEnumerator GetEnumerator() => throw new NotImplementedException();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ ///
+ public int Count => List.Count;
+
+ ///
+ public T this[int index] => List[index];
+
+ void IInputHandler.HandleDeviceConnectionChanged(ConnectionEvent @event) =>
+ HandleDeviceConnectionChanged(@event);
+
+ ///
+ protected internal virtual void HandleDeviceConnectionChanged(ConnectionEvent @event)
+ {
+ if (_list is null || @event.Device is not T t)
+ {
+ return;
+ }
+
+ if (@event.IsConnected)
+ {
+ _list.Add(t);
+ }
+ else
+ {
+ _list.Remove(t);
+ }
+ }
+}
diff --git a/sources/Input/Input/InputMarshal.cs b/sources/Input/Input/InputMarshal.cs
new file mode 100644
index 0000000000..d11ecff97a
--- /dev/null
+++ b/sources/Input/Input/InputMarshal.cs
@@ -0,0 +1,686 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Silk.NET.Input;
+
+///
+/// Contains utilities for creating and manipulating s. This is a very unsafe set of
+/// APIs that are extremely prone to misuse, and therefore is not recommended to be consumed by anyone other than input
+/// backends.
+///
+///
+/// This class is ABI/API stable, but new APIs that obsolete the old ones may be added at any time as more efficient
+/// implementations are discovered.
+///
+// NOTE: Not experimental so that we don't eliminate the prospects of third-party implementations.
+public static class InputMarshal
+{
+ ///
+ /// A wrapper class denoting ownership of a . This is used to attempt to stop
+ /// misuse of these methods, but of course it's fairly trivial to work around this for a user determined to do
+ /// terrible things.
+ ///
+ /// The list element type.
+ public struct ListOwner
+ {
+ internal ListOwner(InputReadOnlyList list) => List = list;
+
+ ///
+ /// Gets the list owned by this owner.
+ ///
+ public InputReadOnlyList List { get; }
+ }
+
+ internal class ButtonList(uint[] binary, Dictionary>? other)
+ : IReadOnlyList>
+ where T : unmanaged, Enum
+ {
+ private Dictionary>? _other = other;
+
+ public ButtonList()
+ : this(new uint[(GetButtonListCount() + 32 - 1) / 32], null) { }
+
+ public ButtonList Clone() =>
+ new([.. binary], _other is null ? null : new Dictionary>(_other));
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+ public IEnumerator> GetEnumerator() =>
+ typeof(T) == typeof(KeyName) ? GetKeyNameEnumerator() : GetButtonEnumerator();
+
+ private IEnumerator> GetKeyNameEnumerator()
+ {
+ var idx = 0;
+ var bit = 0;
+ // To determine the gaps, run the GetButtonCount unit test. The equality check is the LHS from the output +
+ // 1, and the assignment is the RHS - 1. Example output below:
+ // 0 (Unknown), 4 (A)
+ // 129 (VolumeDown), 133 (KeypadComma)
+ // 164 (ExtendSelect), 176 (Keypad00)
+ // 221 (KeypadHexadecimal), 224 (ControlLeft)
+ // 231 (SuperRight), 257 (Mode)
+ // 286 (ApplicationBookmarks), 501 (SoftLeft)
+ for (var cur = (int)KeyName.A; cur <= (int)KeyName.EndCall; cur++)
+ {
+ switch (cur)
+ {
+ case (int)KeyName.VolumeDown + 1:
+ cur = (int)KeyName.KeypadComma - 1;
+ continue;
+ case (int)KeyName.ExtendSelect + 1:
+ cur = (int)KeyName.Keypad00 - 1;
+ continue;
+ case (int)KeyName.KeypadHexadecimal + 1:
+ cur = (int)KeyName.ControlLeft - 1;
+ continue;
+ case (int)KeyName.SuperRight + 1:
+ cur = (int)KeyName.Mode - 1;
+ continue;
+ case (int)KeyName.ApplicationBookmarks + 1:
+ cur = (int)KeyName.SoftLeft - 1;
+ continue;
+ }
+
+ var ret = ElementAt((T)(object)(KeyName)cur, idx, bit);
+ (idx, bit) = BitIterate(idx, bit);
+ yield return ret;
+ }
+ }
+
+ private IEnumerator> GetButtonEnumerator()
+ {
+ var max = GetButtonListCount();
+ int idx = 0,
+ bit = 0;
+ for (var i = 1; i <= max; i++)
+ {
+ var ret = ElementAt(SilkMarshal.ConstCast(i), idx, bit);
+ (idx, bit) = BitIterate(idx, bit);
+ yield return ret;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private Button ElementAt(T name, int idx, int bit)
+ {
+ var ret = new Button(name, false, 0);
+ var isBinaryDown = BitOperations.PopCount(binary[idx] & (1U << (7 - bit))) > 0;
+ if (isBinaryDown)
+ {
+ ret = ret with { IsDown = true, Pressure = 1 };
+ }
+ else
+ {
+ _other?.TryGetValue(name, out ret);
+ }
+
+ return ret;
+ }
+
+ [MethodImpl(
+ MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization
+ )]
+ private static (int, int) BitIterate(int idx, int bit) =>
+ ++bit == 32 ? (++idx, 0) : (idx, bit);
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public int Count => GetButtonListCount();
+
+ [MethodImpl(
+ MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization
+ )]
+ internal static T IndexName(int index)
+ {
+ var name = index;
+ if (typeof(T) == typeof(KeyName))
+ {
+ // To determine the gaps, run the GetButtonCount unit test. The condition is to check whether name
+ // is greater than the LHS, and if so add the RHS less the LHS less 1. Example output:
+ // 0 (Unknown), 4 (A)
+ // 129 (VolumeDown), 133 (KeypadComma)
+ // 164 (ExtendSelect), 176 (Keypad00)
+ // 221 (KeypadHexadecimal), 224 (ControlLeft)
+ // 231 (SuperRight), 257 (Mode)
+ // 286 (ApplicationBookmarks), 501 (SoftLeft)
+ name += 4;
+ if (name > 129)
+ {
+ name += 133 - 129 - 1;
+ }
+
+ if (name > 164)
+ {
+ name += 176 - 164 - 1;
+ }
+
+ if (name > 221)
+ {
+ name += 224 - 221 - 1;
+ }
+
+ if (name > 231)
+ {
+ name += 257 - 231 - 1;
+ }
+
+ if (name > 286)
+ {
+ name += 501 - 286 - 1;
+ }
+ }
+ else
+ {
+ // To account for Unknown = 0.
+ name++;
+ }
+
+ return SilkMarshal.ConstCast(name);
+ }
+
+ [MethodImpl(
+ MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization
+ )]
+ internal static int NameIndex(T name)
+ {
+ var index = SilkMarshal.ConstCast(name);
+ if (typeof(T) == typeof(KeyName))
+ {
+ // To determine the gaps, run the GetButtonCount unit test. The condition is to check whether name
+ // is greater than the LHS, and if so subtract the RHS less the LHS less 1. Note that the conditions
+ // should be in reverse order i.e. from the last output line to the first output line. Example output:
+ // 0 (Unknown), 4 (A)
+ // 129 (VolumeDown), 133 (KeypadComma)
+ // 164 (ExtendSelect), 176 (Keypad00)
+ // 221 (KeypadHexadecimal), 224 (ControlLeft)
+ // 231 (SuperRight), 257 (Mode)
+ // 286 (ApplicationBookmarks), 501 (SoftLeft)
+ if (index > 286)
+ {
+ index -= 501 - 286 - 1;
+ }
+
+ if (index > 231)
+ {
+ index -= 257 - 231 - 1;
+ }
+
+ if (index > 221)
+ {
+ index -= 224 - 221 - 1;
+ }
+
+ if (index > 164)
+ {
+ index -= 176 - 164 - 1;
+ }
+
+ if (index > 129)
+ {
+ index -= 133 - 129 - 1;
+ }
+ index -= 4;
+ }
+ else
+ {
+ // To account for Unknown = 0.
+ index--;
+ }
+
+ return index;
+ }
+
+ public Button this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(index, Count);
+ return ElementAt(
+ IndexName(index),
+ Math.DivRem(index, 32, out var remainder),
+ remainder
+ );
+ }
+ }
+
+ public Button this[T name]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ var index = NameIndex(name);
+ if (index >= 0 && index < GetButtonListCount())
+ {
+ return ElementAt(name, Math.DivRem(index, 32, out var remainder), remainder);
+ }
+
+ Throw();
+ return default;
+ [StackTraceHidden]
+ static void Throw() => throw new ArgumentOutOfRangeException(nameof(name));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Set(Button btn, bool isBinary)
+ {
+ if (btn.IsDown && isBinary)
+ {
+ binary[Math.DivRem(NameIndex(btn.Name), 32, out var bit)] |= 1U << (7 - bit);
+ _other?.Remove(btn.Name);
+ }
+ else
+ {
+ binary[Math.DivRem(NameIndex(btn.Name), 32, out var bit)] &= ~(1U << (7 - bit));
+ }
+
+ if (!isBinary)
+ {
+ (_other ??= [])[btn.Name] = btn;
+ }
+ }
+ }
+
+ ///
+ /// Gets the reported by s created with
+ /// for the given .
+ ///
+ /// The button name type.
+ ///
+ /// The number of buttons that will be in a button list created with , or -1 if
+ /// is not a supported button name type.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ public static int GetButtonListCount()
+ {
+ if (typeof(T) == typeof(JoystickButton))
+ {
+ return (int)JoystickButton.DPadLeft;
+ }
+
+ if (typeof(T) == typeof(PointerButton))
+ {
+ return (int)PointerButton.Button32;
+ }
+
+ if (typeof(T) == typeof(KeyName))
+ {
+ // To determine the ranges, run the GetButtonCount unit test. The RHS of the subtraction statements below
+ // are the RHS of the line output, and the LHS is the LHS of the following line in its output. There is a
+ // final addition that is the number of preceding additions to account for the boundary values. Example
+ // output from that test:
+ // 0 (Unknown), 4 (A)
+ // 129 (VolumeDown), 133 (KeypadComma)
+ // 164 (ExtendSelect), 176 (Keypad00)
+ // 221 (KeypadHexadecimal), 224 (ControlLeft)
+ // 231 (SuperRight), 257 (Mode)
+ // 286 (ApplicationBookmarks), 501 (SoftLeft)
+ // ReSharper disable once ArrangeRedundantParentheses <-- stylistic choice
+ return ((int)KeyName.VolumeDown - (int)KeyName.A)
+ + ((int)KeyName.ExtendSelect - (int)KeyName.KeypadComma)
+ + ((int)KeyName.KeypadHexadecimal - (int)KeyName.Keypad00)
+ + ((int)KeyName.SuperRight - (int)KeyName.ControlLeft)
+ + ((int)KeyName.ApplicationBookmarks - (int)KeyName.Mode)
+ + ((int)KeyName.EndCall - (int)KeyName.SoftLeft)
+ + 6;
+ }
+
+ return -1;
+ }
+
+ ///
+ /// Creates a wrapping the given button .
+ ///
+ /// The list.
+ /// The button name type.
+ /// The button list.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+ public static ButtonReadOnlyList AsButtonList(this InputReadOnlyList> list)
+ where T : unmanaged, Enum => new(list);
+
+ ///
+ /// Creates a wrapping the given .
+ ///
+ /// The list.
+ /// The button name type.
+ /// The button list.
+ public static InputReadOnlyList> AsInputList(this ButtonReadOnlyList list)
+ where T : unmanaged, Enum => new(list);
+
+ ///
+ /// Creates a new for the given , optionally with the
+ /// given where is applicable for this
+ /// .
+ ///
+ /// The capacity.
+ /// The element type.
+ /// The list.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+ public static ListOwner CreateList(int capacity = 0)
+ {
+ if (typeof(T) == typeof(Button))
+ {
+ return (ListOwner)
+ (object)
+ new ListOwner>(
+ new InputReadOnlyList>((object)new ButtonList())
+ );
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return (ListOwner)
+ (object)
+ new ListOwner>(
+ new InputReadOnlyList>(
+ (object)new ButtonList()
+ )
+ );
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return (ListOwner)
+ (object)
+ new ListOwner>(
+ new InputReadOnlyList>(
+ (object)new ButtonList()
+ )
+ );
+ }
+
+ return new ListOwner(new InputReadOnlyList((object)new List(capacity)));
+ }
+
+ ///
+ /// Creates a new from the given . This is
+ /// equivalent to , but returns a
+ /// instead.
+ ///
+ /// The elements to populate the list with.
+ ///
+ ///
+ public static ListOwner Clone(IReadOnlyList other)
+ {
+ // ReSharper disable once InvertIf <-- starting to really dislike this as it duplicates code
+ if (other is InputReadOnlyList irl)
+ {
+ if (typeof(T) == typeof(Button))
+ {
+ return new ListOwner(
+ new InputReadOnlyList(Unsafe.As>(irl.Data).Clone())
+ );
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return new ListOwner(
+ new InputReadOnlyList(Unsafe.As>(irl.Data).Clone())
+ );
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return new ListOwner(
+ new InputReadOnlyList(
+ Unsafe.As>(irl.Data).Clone()
+ )
+ );
+ }
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return new ListOwner(
+ new InputReadOnlyList(CloneButtonList((IReadOnlyList>)other))
+ );
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return new ListOwner(
+ new InputReadOnlyList(
+ CloneButtonList((IReadOnlyList>)other)
+ )
+ );
+ }
+
+ // ReSharper disable once ConvertIfStatementToReturnStatement <-- stylistic choice
+ if (typeof(T) == typeof(Button))
+ {
+ return new ListOwner(
+ new InputReadOnlyList(
+ CloneButtonList((IReadOnlyList>)other)
+ )
+ );
+ }
+
+ return new ListOwner(new InputReadOnlyList((object)new List(other)));
+ static ButtonList CloneButtonList(IReadOnlyList> list)
+ where TEnum : unmanaged, Enum
+ {
+ var ret = new ButtonList();
+ foreach (var button in list)
+ {
+ ret.Set(
+ button,
+ (button.IsDown && button.Pressure >= 1.0)
+ || (!button.IsDown && button.Pressure <= 0.0)
+ );
+ }
+
+ return ret;
+ }
+ }
+
+ ///
+ /// Sets the button state in the given button list.
+ ///
+ /// The list to update.
+ /// The new state of the button.
+ ///
+ /// Whether the of can only be 1.0 when
+ /// is true , and 0.0 when is
+ /// false .
+ ///
+ /// The button type.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+ public static void SetButtonState(ListOwner> list, Button value, bool isBinary)
+ where T : unmanaged, Enum
+ {
+ if (
+ typeof(T) == typeof(KeyName)
+ || typeof(T) == typeof(JoystickButton)
+ || typeof(T) == typeof(PointerButton)
+ )
+ {
+ Unsafe.As>(list.List.Data).Set(value, isBinary);
+ return;
+ }
+
+ var underlying = GetUnderlyingList(list)!;
+ for (var i = 0; i < underlying.Count; i++)
+ {
+ // ReSharper disable once InvertIf <-- this literally results in more lines of code!!!!!
+ if (underlying[i].Name.Equals(value.Name))
+ {
+ underlying[i] = value;
+ return;
+ }
+ }
+
+ underlying.Add(value);
+ }
+
+ ///
+ /// Attempts to retrieve the underlying implementation, provided that
+ /// for the given is implemented as a sequential list
+ /// with individually addressable and a variable number of elements.
+ ///
+ /// The list.
+ /// The list element type.
+ ///
+ /// The list, or null if the optimised implementation of cannot be
+ /// expressed as an .
+ ///
+ ///
+ /// Currently, this can be assumed to not null except for the following types:
+ ///
+ /// where T is
+ /// where T is
+ /// where T is
+ ///
+ /// It is a breaking change to change the underlying implementation of the list such that this method returns
+ /// null where it previously did not return null , therefore Silk.NET will only do this in a
+ /// major release. As a result, it is safe to use the ! operator for code targeting a specific major
+ /// release. Ideally, this is also the case for major releases, but the Silk.NET team cannot guarantee this at this
+ /// time.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+ public static IList? GetUnderlyingList(this ListOwner list) =>
+ typeof(T) == typeof(Button)
+ || typeof(T) == typeof(Button)
+ || typeof(T) == typeof(Button)
+ ? null
+ : Unsafe.As>(list.List.Data);
+
+ ///
+ /// Unsafely creates a for the given list. Note that you should really only do this if
+ /// you are actually the owner of the list and are for some reason not storing the , using
+ /// this API to gain mutable access to an input list is almost always breaking assumptions throughout the input API.
+ ///
+ /// The list.
+ /// The type of the elements in the list.
+ /// The list owner.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ListOwner AsOwned(InputReadOnlyList list) => new(list);
+
+ ///
+ /// Given an owned mutable , swaps out the value of .
+ /// Note that this should not be used unexpectedly, and if there are any doubts about ownership or usage then create
+ /// a new instance of for safe mutation.
+ ///
+ /// The .
+ /// The new triggers.
+ public static void SetTriggers(GamepadState state, DualReadOnlyList triggers) =>
+ state.Triggers = triggers;
+
+ ///
+ /// Given an owned mutable , swaps out the value of
+ /// . Note that this should not be used unexpectedly, and if there are any
+ /// doubts about ownership or usage then create a new instance of for safe mutation.
+ ///
+ /// The .
+ /// The new thumbsticks.
+ public static void SetThumbsticks(GamepadState state, DualReadOnlyList thumbsticks) =>
+ state.Thumbsticks = thumbsticks;
+
+ ///
+ /// Given an owned mutable , swaps out the value of
+ /// . Note that this should not be used unexpectedly, and if there are any
+ /// doubts about ownership or usage then create a new instance of for safe mutation.
+ ///
+ /// The .
+ /// The new grip pressure.
+ public static void SetGripPressure(PointerState state, float gripPressure) =>
+ state.GripPressure = gripPressure;
+
+ // These are APIs defined on InputReadOnlyList or ButtonReadOnlyList but are implemented here to keep the
+ // implementation of the backing list in one file, the hope being that this decreases the likelihood of bugs.
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+ internal static Button GetButtonState(InputReadOnlyList> list, T name)
+ where T : unmanaged, Enum
+ {
+ if (
+ typeof(T) == typeof(KeyName)
+ || typeof(T) == typeof(JoystickButton)
+ || typeof(T) == typeof(PointerButton)
+ )
+ {
+ return Unsafe.As>(list.Data)[name];
+ }
+
+ var underlying = Unsafe.As>>(list.Data);
+ foreach (var t in underlying)
+ {
+ // ReSharper disable once InvertIf <-- this literally results in more lines of code!!!!!
+ if (t.Name.Equals(name))
+ {
+ return t;
+ }
+ }
+
+ return new Button(name, false, 0);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static int GetListCount(InputReadOnlyList list)
+ {
+ if (typeof(T) == typeof(Button))
+ {
+ return GetButtonListCount();
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return GetButtonListCount();
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return GetButtonListCount();
+ }
+
+ return Unsafe.As>(list.Data).Count;
+ }
+
+ // ReSharper disable NotDisposedResourceIsReturned - Nope, sorry, not adding a reference to JetBrains.Annotations.
+ internal static IEnumerator EnumerateList(InputReadOnlyList list)
+ {
+ if (typeof(T) == typeof(Button))
+ {
+ return (IEnumerator)Unsafe.As>(list.Data).GetEnumerator();
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return (IEnumerator)Unsafe.As>(list.Data).GetEnumerator();
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return (IEnumerator)Unsafe.As>(list.Data).GetEnumerator();
+ }
+
+ return Unsafe.As>(list.Data).GetEnumerator();
+ } // ReSharper restore NotDisposedResourceIsReturned
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static T ElementAt(InputReadOnlyList list, int index)
+ {
+ if (typeof(T) == typeof(Button))
+ {
+ return (T)(object)Unsafe.As>(list.Data)[index];
+ }
+
+ if (typeof(T) == typeof(Button))
+ {
+ return (T)(object)Unsafe.As