diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 03efa87b74..393d187924 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,10 +3,11 @@
   "isRoot": true,
   "tools": {
     "csharpier": {
-      "version": "0.29.2",
+      "version": "0.30.6",
       "commands": [
         "dotnet-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 @@
     <PackageReference Include="Octokit" Version="13.0.1" />
     <PackageReference Include="System.Linq.Async" Version="6.0.1" />
   </ItemGroup>
+
+  <ItemGroup>
+    <Content Include="..\..\tests\Input\Input\Silk.NET.Input.UnitTests.csproj">
+      <Link>Directory.Build\tests\Input\Input\Silk.NET.Input.UnitTests.csproj</Link>
+    </Content>
+  </ItemGroup>
 </Project>
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;
+
+/// <summary>
+/// Represents a button the user can push.
+/// </summary>
+/// <param name="Name">The name of the button.</param>
+/// <param name="IsDown">Whether the user is pushing the button.</param>
+/// <param name="Pressure">
+/// The pressure with which the user is pushing the button, where <c>0.0</c> is the smallest measurable pressure and
+/// <c>1.0</c> is the largest measurable pressure.
+/// </param>
+/// <typeparam name="T">
+/// The button type (e.g. <see cref="JoystickButton"/>, <see cref="PointerButton"/>, etc).
+/// </typeparam>
+public readonly record struct Button<T>(T Name, bool IsDown, float Pressure)
+    where T : unmanaged, Enum
+{
+    /// <summary>
+    /// Collapses this <see cref="Button{T}"/> struct into just its <see cref="IsDown"/> value.
+    /// </summary>
+    /// <param name="state">The button state.</param>
+    /// <returns>The <see cref="IsDown"/> value.</returns>
+    public static implicit operator bool(Button<T> 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;
+
+/// <summary>
+/// Contains information pertaining to a button state change (e.g. press, depress, etc).
+/// </summary>
+/// <param name="Device">The device on which the button being pressed or depressed resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Button">The new state of the button being pressed or depressed.</param>
+/// <param name="Previous">The previous state of the button.</param>
+/// <typeparam name="T">The button type e.g. <see cref="JoystickButton"/>, <see cref="PointerButton"/>, etc.</typeparam>
+public readonly record struct ButtonChangedEvent<T>(
+    IButtonDevice<T> Device,
+    long Timestamp,
+    Button<T> Button,
+    Button<T> 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;
+
+/// <summary>
+/// An implementation of <see cref="IReadOnlyList{T}"/> providing utility APIs for getting a <see cref="Button{T}"/>
+/// given a button name <typeparamref name="T"/>, that is optimised for storing <see cref="Button{T}"/>s with the
+/// given button name type <typeparamref name="T"/> using the most memory-efficient mechanism available.
+/// </summary>
+/// <typeparam name="T">
+/// The button type (e.g. <see cref="JoystickButton"/>, <see cref="PointerButton"/>, etc).
+/// </typeparam>
+public struct ButtonReadOnlyList<T> : IReadOnlyList<Button<T>>
+    where T : unmanaged, Enum
+{
+    private InputReadOnlyList<Button<T>> _list;
+
+    internal ButtonReadOnlyList(InputReadOnlyList<Button<T>> list) => _list = list;
+
+    /// <summary>
+    /// Creates an <see cref="ButtonReadOnlyList{T}"/> from a <see cref="IReadOnlyList{T}"/>.
+    /// </summary>
+    /// <param name="other">The list to copy.</param>
+    public ButtonReadOnlyList(IReadOnlyList<Button<T>> other) =>
+        InputMarshal.Clone(other).List.AsButtonList();
+
+    /// <summary>
+    /// Gets the state for the button with the given name.
+    /// </summary>
+    /// <param name="name">The button name.</param>
+    public Button<T> this[T name] => InputMarshal.GetButtonState(_list, name);
+
+    /// <inheritdoc />
+    public IEnumerator<Button<T>> GetEnumerator() => _list.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+    /// <inheritdoc />
+    public int Count => _list.Count;
+
+    /// <inheritdoc />
+    public Button<T> 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;
+
+/// <summary>
+/// Contains information pertaining to a device connection or disconnection event.
+/// </summary>
+/// <param name="Device">The device that has disconnected or connected.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="IsConnected">Whether the device has connected (<c>true</c>) or disconnected (<c>false</c>).</param>
+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;
+
+/// <summary>
+/// Enumerates the modes in which a mouse cursor can operate.
+/// </summary>
+/// <remarks>
+/// <see cref="IPointerDevice"/> implementations for <see cref="IMouse"/> implementations typically have two
+/// <see cref="IPointerDevice.Targets"/>:
+/// <list type="bullet">
+/// <item>
+/// <term>Bounded <see cref="IPointerTarget"/></term>
+/// <description>
+/// An <see cref="IPointerTarget"/> that is bounded to the desktop environment i.e. the
+/// <see cref="IPointerTarget.Bounds"/> 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 <see cref="TargetPoint"/>.
+/// This target is used for every cursor mode except <see cref="Unbounded"/>.
+/// </description>
+/// </item>
+/// <item>
+/// <term>Unbounded <see cref="IPointerTarget"/></term>
+/// <description>
+/// An <see cref="IPointerTarget"/> that is unbounded and operates in an arbitrary coordinate space. This target is used
+/// for <b>raw mouse mode</b> and points on this target represent the net mouse movement from a mouse. Implementations
+/// are more likely to be able to give multiple <see cref="TargetPoint"/>s for each mouse when this target is used. This
+/// target is used when the <see cref="Unbounded"/> cursor mode is enabled. <see cref="IPointerTarget.Bounds"/> will
+/// represent an infinitely large unbounded target.
+/// </description>
+/// </item>
+/// </list>
+/// </remarks>
+[Flags]
+public enum CursorModes
+{
+    /// <summary>
+    /// The cursor is visible to the user and operating within the bounds of the <b>desktop environment</b>. The
+    /// coordinates received are in desktop coordinates, operating in the same coordinate space as the window
+    /// position/size.
+    /// </summary>
+    Normal = 1 << 0,
+
+    /// <summary>
+    /// The cursor is visible to the user but is constrained to the <b>window's client area</b>. The coordinates
+    /// received are in desktop coordinates, operating in the same coordinate space as the window position/size.
+    /// The <see cref="IPointerTarget"/> bounded to the desktop environment is used.
+    /// </summary>
+    Confined = 1 << 1,
+
+    /// <summary>
+    /// The cursor is invisible to the user and is <b>unconstrained/unbounded</b>. The coordinates received are
+    /// arbitrary values that have no bounds representing the net mouse movement since entering into this cursor mode.
+    /// The unbounded <see cref="IPointerTarget"/> is used. This is the equivalent of <b>raw mouse mode</b>.
+    /// </summary>
+    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;
+
+/// <summary>
+/// Enumerates the cursor styles with which the desktop environment should render the cursor.
+/// </summary>
+[Flags]
+public enum CursorStyles
+{
+    /// <summary>
+    /// The cursor should be rendered using its default image.
+    /// </summary>
+    Default,
+
+    /// <summary>
+    /// The cursor should be rendered using an arrow cursor image.
+    /// </summary>
+    Arrow = 1 << 0,
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    IBeam = 1 << 1,
+
+    /// <summary>
+    /// The cursor should be rendered using a crosshair cursor image.
+    /// </summary>
+    Crosshair = 1 << 2,
+
+    /// <summary>
+    /// The cursor should be rendered using a hand cursor image, typically used when hovering over a web link.
+    /// </summary>
+    Hand = 1 << 3,
+
+    /// <summary>
+    /// The cursor should be rendered using a two-headed horizontal sizing cursor image.
+    /// </summary>
+    HResize = 1 << 4,
+
+    /// <summary>
+    /// The cursor should be rendered using a two-headed vertical sizing cursor image.
+    /// </summary>
+    VResize = 1 << 5,
+
+    /// <summary>
+    /// The cursor should not be rendered.
+    /// </summary>
+    /// <remarks>
+    /// When <see cref="CursorModes.Unbounded"/> is used, the cursor ceases to exist anyway. As such, while the
+    /// <see cref="ICursorConfiguration.Style"/> property may not reflect this (as it is retained across changes to
+    /// <see cref="ICursorConfiguration.Mode"/> and just ignored when <see cref="CursorModes.Unbounded"/> is used),
+    /// <see cref="ICursorConfiguration.Style"/> can be implied as being <see cref="CursorStyles.Hidden"/> when
+    /// <see cref="CursorModes.Unbounded"/> is used.
+    /// </remarks>
+    Hidden = 1 << 6,
+
+    /// <summary>
+    /// The cursor should be rendered using a custom application-provided image.
+    /// </summary>
+    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..8780f934af
--- /dev/null
+++ b/sources/Input/Input/CustomCursor.cs
@@ -0,0 +1,22 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Represents a custom image for a mouse cursor.
+/// </summary>
+public readonly ref struct CustomCursor
+{
+    /// <summary>
+    /// The number of pixels in the X axis.
+    /// </summary>
+    public int Width { get; init; }
+
+    /// <summary>
+    /// The number of pixels in the Y axis.
+    /// </summary>
+    public int Height { get; init; }
+
+    /// <summary>
+    /// The row-major 32-bit RGBA pixel data (i.e. 8 bytes for each colour component).
+    /// </summary>
+    public ReadOnlySpan<int> Data { get; init; } // Rgba32
+}
\ No newline at end of file
diff --git a/sources/Input/Input/DualReadOnlyList.cs b/sources/Input/Input/DualReadOnlyList.cs
new file mode 100644
index 0000000000..9639703823
--- /dev/null
+++ b/sources/Input/Input/DualReadOnlyList.cs
@@ -0,0 +1,39 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Represents a list that has exactly two elements.
+/// </summary>
+/// <typeparam name="T">The element type.</typeparam>
+public readonly struct DualReadOnlyList<T> : IReadOnlyList<T>
+{
+    /// <summary>
+    /// The first/leftmost element.
+    /// </summary>
+    public readonly T Left;
+
+    /// <summary>
+    /// The second/rightmost element.
+    /// </summary>
+    public readonly T Right;
+
+    /// <inheritdoc />
+    public IEnumerator<T> GetEnumerator()
+    {
+        yield return Left;
+        yield return Right;
+    }
+
+    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+    /// <inheritdoc />
+    public int Count => 2;
+
+    /// <inheritdoc />
+    public T this[int index] => index switch {
+        0 => Left,
+        1 => Right,
+        _ => throw new IndexOutOfRangeException()
+    };
+}
\ No newline at end of file
diff --git a/sources/Input/Input/GamepadState.cs b/sources/Input/Input/GamepadState.cs
new file mode 100644
index 0000000000..05d0d9bb2a
--- /dev/null
+++ b/sources/Input/Input/GamepadState.cs
@@ -0,0 +1,24 @@
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains user input received from an <see cref="IGamepad"/>.
+/// </summary>
+public class GamepadState
+{
+    /// <summary>
+    /// Gets the gamepad button state denoting the buttons being pressed or depressed.
+    /// </summary>
+    public ButtonReadOnlyList<JoystickButton> Buttons { get; }
+
+    /// <summary>
+    /// Gets the state of the twin sticks on the gamepad.
+    /// </summary>
+    public DualReadOnlyList<Vector2> Thumbsticks { get; }
+
+    /// <summary>
+    /// Gets the state of the triggers on the gamepad.
+    /// </summary>
+    public DualReadOnlyList<float> Triggers { get; }
+}
\ No newline at end of file
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;
+
+/// <summary>
+/// Contains information pertaining to the movement of a thumbstick.
+/// </summary>
+/// <param name="Gamepad">The gamepad on which the thumbstick resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Value">
+/// The new position of the thumbstick, where each axis is between <c>-1.0</c> and <c>1.0</c>.
+/// </param>
+/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param>
+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;
+
+/// <summary>
+/// Contains information pertaining to the movement of a trigger.
+/// </summary>
+/// <param name="Gamepad">The gamepad on which the trigger resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Axis">The index of the trigger that has moved.</param>
+/// <param name="Value">
+/// The new value of the trigger, between <c>0.0</c> (fully depressed) and <c>1.0</c> (fully pressed).
+/// </param>
+/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param>
+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;
+
+/// <summary>
+/// Represents a collection of <see cref="IGamepad"/>s from which input events can be received.
+/// </summary>
+public sealed class Gamepads : InputContextDeviceList<IGamepad>, IGamepadInputHandler
+{
+    internal Gamepads(InputContext ctx)
+        : base(ctx) { }
+
+    /// <summary>
+    /// Raised when state pertaining to a pushable button on the gamepad changes (e.g. button up, button down).
+    /// </summary>
+    public event Action<ButtonChangedEvent<JoystickButton>>? ButtonChanged;
+
+    /// <summary>
+    /// Raised when a thumbstick on the gamepad moves.
+    /// </summary>
+    public event Action<GamepadThumbstickMoveEvent>? ThumbstickMove;
+
+    /// <summary>
+    /// Raised when a trigger on the gamepad moves.
+    /// </summary>
+    public event Action<GamepadTriggerMoveEvent>? TriggerMove;
+
+    internal void HandleButtonChanged(ButtonChangedEvent<JoystickButton> @event) =>
+        ButtonChanged?.Invoke(@event);
+
+    void IButtonInputHandler<JoystickButton>.HandleButtonChanged(
+        ButtonChangedEvent<JoystickButton> @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;
+
+/// <summary>
+/// Represents an input device that has buttons.
+/// </summary>
+/// <typeparam name="T">The type of buttons the input device has.</typeparam>
+public interface IButtonDevice<T> : IInputDevice
+    where T : unmanaged, Enum
+{
+    /// <summary>
+    /// Gets the current button state for this device.
+    /// </summary>
+    /// <remarks>
+    /// Only updated when <see cref="IInputBackend.Update"/> is called.
+    /// </remarks>
+    ButtonReadOnlyList<T> 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;
+
+/// <summary>
+/// An <see cref="IInputHandler"/> that also receives <see cref="IButtonDevice{T}"/> events.
+/// </summary>
+/// <typeparam name="T">The device's button type.</typeparam>
+public interface IButtonInputHandler<T> : IInputHandler
+    where T : unmanaged, Enum
+{
+    /// <summary>
+    /// Called when a button's state changes (e.g. button down, button up).
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    void HandleButtonChanged(ButtonChangedEvent<T> @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;
+
+/// <summary>
+/// Configuration for the behaviour of a mouse cursor.
+/// </summary>
+public interface ICursorConfiguration
+{
+    /// <summary>
+    /// Gets a bitmask denoting the supported values for <see cref="Mode"/>.
+    /// </summary>
+    CursorModes SupportedModes { get; }
+
+    /// <summary>
+    /// Gets or sets the current cursor mode. Only one bit shall be set at a time.
+    /// </summary>
+    /// <remarks>
+    /// Note that this property affects the <see cref="IPointerDevice.Targets"/> in use, see the
+    /// <see cref="CursorModes"/> documentation for more info.
+    /// </remarks>
+    CursorModes Mode { get; set; }
+
+    /// <summary>
+    /// Gets a bitmask denoting the supported values for <see cref="Style"/>.
+    /// </summary>
+    CursorStyles SupportedStyles { get; }
+
+    /// <summary>
+    /// Gets or sets the current cursor style. Only one bit shall be set at a time. <see cref="CursorStyles.Custom"/>
+    /// shall use the <see cref="Image"/> provided.
+    /// </summary>
+    /// <remarks>
+    /// When <see cref="CursorModes.Unbounded"/> is used, the cursor ceases to exist anyway. As such, while the
+    /// <see cref="ICursorConfiguration.Style"/> property may not reflect this (as it is retained across changes to
+    /// <see cref="ICursorConfiguration.Mode"/> and just ignored when <see cref="CursorModes.Unbounded"/> is used),
+    /// <see cref="ICursorConfiguration.Style"/> can be implied as being <see cref="CursorStyles.Hidden"/> when
+    /// <see cref="CursorModes.Unbounded"/> is used.
+    /// </remarks>
+    CursorStyles Style { get; set; }
+
+    /// <summary>
+    /// Gets or sets the current custom cursor image. This has no effect if <see cref="CursorStyles.Custom"/> is not
+    /// used, but the value is stored nonetheless for use when that is the case.
+    /// </summary>
+    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;
+
+/// <summary>
+/// Represents a gamepad that follows a typical layout.
+/// </summary>
+public interface IGamepad : IButtonDevice<JoystickButton>
+{
+    /// <summary>
+    /// Gets the device state.
+    /// </summary>
+    /// <remarks>
+    /// Only updated when <see cref="IInputBackend.Update"/> is called.
+    /// </remarks>
+    new GamepadState State { get; }
+    ButtonReadOnlyList<JoystickButton> IButtonDevice<JoystickButton>.State => State.Buttons;
+    /// <summary>
+    /// Gets a collection enumerating the vibration motors available to the application to enable haptics.
+    /// </summary>
+    IReadOnlyList<IMotor> 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;
+
+/// <summary>
+/// An <see cref="IInputHandler"/> that also receives <see cref="IGamepad"/> input.
+/// </summary>
+public interface IGamepadInputHandler : IButtonInputHandler<JoystickButton>
+{
+    /// <summary>
+    /// Called when one of the twin sticks moves.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    void HandleThumbstickMove(GamepadThumbstickMoveEvent @event);
+
+    /// <summary>
+    /// Called when one of the two triggers moves.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    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;
+
+/// <summary>
+/// Represents an input backend capable of receiving human input from Human Input Devices (HIDs).
+/// </summary>
+/// <remarks>
+/// 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 <see cref="Update"/> can be called
+/// on - the user is responsible for respecting these threading rules as well.
+/// </remarks>
+public interface IInputBackend : IDisposable
+{
+    /// <summary>
+    /// Gets a rough human-readable description of the input backend. Its value is not intrinsically meaningful.
+    /// </summary>
+    string Name { get; }
+
+    /// <summary>
+    /// Gets a globally-unique integral identifier for this device.
+    /// </summary>
+    nint Id { get; }
+
+    /// <summary>
+    /// Get a list containing all the <b>connected</b> devices available from this input backend.
+    /// </summary>
+    /// <remarks>
+    /// When a device is disconnected, its <see cref="IInputDevice"/> shall no longer function and will not be
+    /// enumerated by this list. When a device is connected, an <see cref="IInputDevice"/> with that physical device ID
+    /// shall be added to this list. In addition, upon connection any past <see cref="IInputDevice"/> objects previously
+    /// enumerated by this list on this <see cref="IInputBackend"/> 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 <see cref="IInputDevice"/> 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 <see cref="Devices"/> (checked using <see cref="IInputDevice"/>s
+    /// <see cref="IEquatable{IInputDevice}"/> implementation) list is sufficient evidence that a device has been
+    /// disconnected.
+    /// </remarks>
+    IReadOnlyList<IInputDevice> Devices { get; }
+
+    /// <summary>
+    /// Polls and updates the state of the <see cref="IInputDevice"/> objects connected using this backend, sending
+    /// input events to the given <see cref="IInputHandler"/> to reflect the human input received.
+    /// </summary>
+    /// <remarks>
+    /// The value of the <c>State</c> properties on each device must not change until this method is called.
+    /// </remarks>
+    /// <param name="handler">The input handler.</param>
+    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;
+
+/// <summary>
+/// Represents a connected Human Input Device (HID).
+/// </summary>
+/// <remarks>
+/// All devices originate from a backend.<br />
+/// <br />
+/// An <see cref="IInputDevice"/> object shall be equatable to any such object retrieved from the same backend where
+/// <see cref="Id"/> is equal.<br />
+/// <br />
+/// <see cref="IInputDevice"/> 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 <see cref="IInputDevice"/> objects are equatable based on <see cref="Id"/>s, if a physical
+/// device disconnects and reconnects the <see cref="IInputBackend"/> does not provide a guarantee that the same object
+/// will be returned (primarily because doing so would require the <see cref="IInputBackend"/> 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 <see cref="IInputBackend"/>
+/// implementation instead in the form of a mapping of physical device IDs (<see cref="Id"/>) to those handles. This
+/// solves the object lifetime problem while also not adding undue complications to user code.
+/// </remarks>
+public interface IInputDevice : IEquatable<IInputDevice>
+{
+    /// <summary>
+    /// Gets a globally-unique integral identifier for this device.
+    /// </summary>
+    nint Id { get; }
+
+    /// <summary>
+    /// Gets a rough human-readable description of the input device. Its value is not intrinsically meaningful.
+    /// </summary>
+    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;
+
+/// <summary>
+/// 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.
+/// </summary>
+public interface IInputHandler
+{
+    /// <summary>
+    /// Called when an <see cref="IInputDevice"/> disconnects from the application.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    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;
+
+/// <summary>
+/// Represents a joystick with axes, buttons, and hats.
+/// </summary>
+public interface IJoystick : IButtonDevice<JoystickButton>
+{
+    /// <summary>
+    /// Gets the device state.
+    /// </summary>
+    /// <remarks>
+    /// Only updated when <see cref="IInputBackend.Update"/> is called.
+    /// </remarks>
+    new JoystickState State { get; }
+    ButtonReadOnlyList<JoystickButton> IButtonDevice<JoystickButton>.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;
+
+/// <summary>
+/// An <see cref="IInputHandler"/> that also receives <see cref="IJoystick"/> input.
+/// </summary>
+public interface IJoystickInputHandler : IButtonInputHandler<JoystickButton>
+{
+    /// <summary>
+    /// Called when an axis on the joystick moves.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    void HandleAxisMove(JoystickAxisMoveEvent @event);
+    
+    /// <summary>
+    /// Called when a hat on the joystick moves.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    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;
+
+/// <summary>
+/// Represents a keyboard device.
+/// </summary>
+public interface IKeyboard : IButtonDevice<KeyName>
+{
+    /// <summary>
+    /// Gets the device state.
+    /// </summary>
+    /// <remarks>
+    /// Only updated when <see cref="IInputBackend.Update"/> is called.
+    /// </remarks>
+    new KeyboardState State { get; }
+
+    ButtonReadOnlyList<KeyName> IButtonDevice<KeyName>.State => State.Keys;
+
+    /// <summary>
+    /// Gets or sets the current text on the clipboard.
+    /// </summary>
+    string? ClipboardText { get; set; }
+
+    /// <summary>
+    /// Attempts to get a user-displayable string in the user's locale for the key at the physical position represented
+    /// by <paramref name="key"/> in the user's current keyboard layout.
+    /// </summary>
+    /// <param name="key">The physical key name. Consult <see cref="KeyName"/> documentation for more info.</param>
+    /// <param name="name">The user-displayable name of the key.</param>
+    /// <returns>Whether the name was successfully retrieved.</returns>
+    bool TryGetKeyName(KeyName key, [NotNullWhen(true)] out string? name);
+
+    /// <summary>
+    /// Begins recording keyboard input. Without <see cref="BeginInput"/>/<see cref="EndInput"/>, there is no
+    /// guarantee that <see cref="IKeyboardInputHandler.HandleKeyChar"/> will be raised as this might require displaying
+    /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call
+    /// <see cref="BeginInput"/> when you'd like to capture text input (e.g. in a text box), followed by
+    /// <see cref="EndInput"/> when you have completed collecting such input.
+    /// </summary>
+    void BeginInput();
+
+    /// <summary>
+    /// Concludes recording keyboard input. Without <see cref="BeginInput"/>/<see cref="EndInput"/>, there is no
+    /// guarantee that <see cref="IKeyboardInputHandler.HandleKeyChar"/> will be raised as this might require displaying
+    /// a concept/touchscreen keyboard on certain platforms (e.g. phones). It is recommended that you call
+    /// <see cref="BeginInput"/> when you'd like to capture text input (e.g. in a text box), followed by
+    /// <see cref="EndInput"/> when you have completed collecting such input.
+    /// </summary>
+    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;
+
+/// <summary>
+/// An <see cref="IInputHandler"/> that also receives <see cref="IKeyboard"/> events.
+/// </summary>
+public interface IKeyboardInputHandler : IButtonInputHandler<KeyName>
+{
+    /// <summary>
+    /// Called when a key is pressed or depressed.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    void HandleKeyChanged(KeyChangedEvent @event);
+
+    /// <summary>
+    /// Called when a character is typed.
+    /// </summary>
+    /// <remarks>
+    /// Ensure you have called <see cref="IKeyboard.BeginInput"/> to start receiving text, after which events will be
+    /// sent for each character until <see cref="IKeyboard.EndInput"/> is called.
+    /// </remarks>
+    /// <param name="event">The event details.</param>
+    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;
+
+/// <summary>
+/// Represents a vibration motor.
+/// </summary>
+public interface IMotor
+{
+    /// <summary>
+    /// Gets or sets the speed at which the motor is operating, where <c>0.0</c> represents no vibration and <c>1.0</c>
+    /// represents the maximum amount of vibration.
+    /// </summary>
+    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;
+
+/// <summary>
+/// Represents a mouse - a type of pointer device.
+/// </summary>
+public interface IMouse : IPointerDevice
+{
+    /// <summary>
+    /// Gets the device state.
+    /// </summary>
+    /// <remarks>
+    /// Only updated when <see cref="IInputBackend.Update"/> is called.
+    /// </remarks>
+    new MouseState State { get; }
+
+    PointerState IPointerDevice.State => State;
+
+    /// <summary>
+    /// Gets the cursor configuration of the mouse.
+    /// </summary>
+    /// <remarks>
+    /// This will determine which <see cref="IPointerTarget"/> points shall lie on.
+    /// </remarks>
+    ICursorConfiguration Cursor { get; }
+
+    /// <summary>
+    /// Attempts to set the position of the mouse.
+    /// </summary>
+    /// <param name="position">The position of the mouse in window coordinates.</param>
+    /// <returns>Whether the requested position has been applied.</returns>
+    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;
+
+/// <summary>
+/// An <see cref="IInputHandler"/> that receives input from an <see cref="IMouse"/>.
+/// </summary>
+public interface IMouseInputHandler : IButtonInputHandler<PointerButton>
+{
+    /// <summary>
+    /// Called when the user scrolls using the scroll wheel.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    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;
+
+/// <summary>
+/// Represents a device with which the user can point at a target.
+/// </summary>
+public interface IPointerDevice : IButtonDevice<PointerButton>
+{
+    /// <summary>
+    /// Gets the device state.
+    /// </summary>
+    /// <remarks>
+    /// Only updated when <see cref="IInputBackend.Update"/> is called.
+    /// </remarks>
+    new PointerState State { get; }
+    ButtonReadOnlyList<PointerButton> IButtonDevice<PointerButton>.State => State.Buttons;
+    /// <summary>
+    /// Gets the targets at which the user can point with their pointer.
+    /// </summary>
+    IReadOnlyList<IPointerTarget> 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;
+
+/// <summary>
+/// An <see cref="IInputHandler"/> that also receives <see cref="IPointerDevice"/> events.
+/// </summary>
+public interface IPointerInputHandler : IButtonInputHandler<PointerButton>
+{
+    /// <summary>
+    /// 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.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    void HandleTargetChanged(PointerTargetChangedEvent @event);
+
+    /// <summary>
+    /// Called when the user adds, removes, or changes a point at which they're pointing at a target.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    void HandlePointChanged(PointChangedEvent @event);
+
+    /// <summary>
+    /// Called when the user changes the pressure with which they're gripping the pointer device.
+    /// </summary>
+    /// <param name="event">The event details.</param>
+    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;
+
+/// <summary>
+/// Represents a target at which the user can point using their pointer device.
+/// </summary>
+public interface IPointerTarget
+{
+    /// <summary>
+    /// The boundary in which positions of points on this target shall fall. For <see cref="Box3D{T}.Min" />,
+    /// <see cref="float.NegativeInfinity" /> shall represent the lack of a lower bound on a particular axis. For
+    /// For <see cref="Box3D{T}.Max" />, <see cref="float.PositiveInfinity" /> shall represent the lack of a lower bound
+    /// on a particular axis. <c>0</c> represents an unused axis that axis is <c>0</c> on both
+    /// <see cref="Box3D{T}.Min" /> and <see cref="Box3D{T}.Max" />.
+    /// </summary>
+    Box3D<float> Bounds { get; }
+
+    /// <summary>
+    /// Gets the number of points with which the given pointer is pointing at this target.
+    /// </summary>
+    /// <returns>The number of points.</returns>
+    /// <remarks>
+    /// 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.
+    /// </remarks>
+    int GetPointCount(IPointerDevice pointer);
+
+    /// <summary>
+    /// Gets a point with which the given pointer is pointing at this target.
+    /// </summary>
+    /// <param name="pointer">The pointer device.</param>
+    /// <param name="point">
+    /// The index of the point, between <c>0</c> and the number sourced from <see cref="GetPointCount" />.
+    /// </param>
+    /// <returns>The point at the given index with which the given pointer device is pointing at the target.</returns>
+    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..9d2f55ce37
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/InputWindowExtensions.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.
+
+// ReSharper disable CheckNamespace
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains extensions for creating input backends and contexts from <see cref="INativeWindow"/>s.
+/// </summary>
+public static partial class InputWindowExtensions
+{
+    public static partial IInputBackend CreateInputBackend(this INativeWindow window)
+    {
+        throw new NotImplementedException();
+    }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs b/sources/Input/Input/Implementations/SDL3/SdlGamepad.cs
new file mode 100644
index 0000000000..64cd37ba8a
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlGamepad.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 SdlGamepad : IGamepad
+{
+    public bool Equals(IInputDevice? other) => throw new NotImplementedException();
+
+    public IntPtr Id => throw new NotImplementedException();
+
+    public string Name => throw new NotImplementedException();
+
+    public GamepadState State => throw new NotImplementedException();
+
+    public IReadOnlyList<IMotor> VibrationMotors => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs
new file mode 100644
index 0000000000..24d0a8fdd9
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlInputBackend.cs
@@ -0,0 +1,47 @@
+// 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.SDL;
+
+namespace Silk.NET.Input.SDL3;
+
+internal class SdlInputBackend : IInputBackend
+{
+    public unsafe SdlInputBackend()
+    {
+        var ptr = new EventFilter(OnEvent);
+        Sdl.AddEventWatch(ptr, nullptr);
+        Id = (nint)ptr.Handle;
+    }
+
+    private unsafe byte OnEvent(void* arg0, Event* arg1)
+    {
+        throw new NotImplementedException();
+    }
+
+    public string Name =>
+        $"Silk.NET.Input Reference Implementation using SDL3 ({Sdl.GetPlatform().ReadToString()})";
+
+    public nint Id { get; }
+
+    public IReadOnlyList<IInputDevice> Devices => throw new NotImplementedException();
+
+    public void Update(IInputHandler? handler = null) => throw new NotImplementedException();
+
+    private unsafe void ReleaseUnmanagedResources()
+    {
+        Sdl.RemoveEventWatch(
+            new EventFilter((delegate* unmanaged<void*, Event*, byte>)(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..b4ecb54675
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlJoystick.cs
@@ -0,0 +1,15 @@
+// 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 : IJoystick
+{
+    public bool Equals(IInputDevice? other) => throw new NotImplementedException();
+
+    public IntPtr Id => throw new NotImplementedException();
+
+    public 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..9bd7de6d81
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlKeyboard.cs
@@ -0,0 +1,30 @@
+// 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 : IKeyboard
+{
+    public bool Equals(IInputDevice? other) => throw new NotImplementedException();
+
+    public IntPtr Id => throw new NotImplementedException();
+
+    public 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..3e551826a6
--- /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 : IMotor
+{
+    public float Speed
+    {
+        get => throw new NotImplementedException();
+        set => throw new NotImplementedException();
+    }
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlMouse.cs b/sources/Input/Input/Implementations/SDL3/SdlMouse.cs
new file mode 100644
index 0000000000..25ac5cdb4f
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlMouse.cs
@@ -0,0 +1,23 @@
+// 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 SdlMouse : IMouse
+{
+    public bool Equals(IInputDevice? other) => throw new NotImplementedException();
+
+    public IntPtr Id => throw new NotImplementedException();
+
+    public string Name => throw new NotImplementedException();
+
+    public MouseState State => throw new NotImplementedException();
+
+    public ICursorConfiguration Cursor => throw new NotImplementedException();
+
+    public bool TrySetPosition(Vector2 position) => throw new NotImplementedException();
+
+    public IReadOnlyList<IPointerTarget> Targets => throw new NotImplementedException();
+}
diff --git a/sources/Input/Input/Implementations/SDL3/SdlPen.cs b/sources/Input/Input/Implementations/SDL3/SdlPen.cs
new file mode 100644
index 0000000000..d061bc86ca
--- /dev/null
+++ b/sources/Input/Input/Implementations/SDL3/SdlPen.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 SdlPen : 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<IPointerTarget> Targets => throw new NotImplementedException();
+}
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<IPointerTarget> Targets => 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;
+
+/// <summary>
+/// Represents an "input context" containing multiple <see cref="IInputBackend"/>s from which
+/// <see cref="IInputDevice"/>s, their state, and their events are aggregated and laid-out in a user-friendly fashion.
+/// </summary>
+/// <remarks>
+/// 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 <see cref="Update"/> can be called
+/// on - the user is responsible for respecting these threading rules as well.
+/// </remarks>
+public class InputContext
+    : IJoystickInputHandler,
+        IGamepadInputHandler,
+        IMouseInputHandler,
+        IPointerInputHandler,
+        IKeyboardInputHandler,
+        IList<IInputBackend>
+{
+    // 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<IInputBackend> _backends = [];
+    private List<IInputDevice>? _devices;
+
+    /// <summary>
+    /// Gets the <see cref="IPointerDevice"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context.
+    /// </summary>
+    public Pointers Pointers => _pointers ??= new Pointers(this);
+
+    /// <summary>
+    /// Gets the <see cref="IKeyboard"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context.
+    /// </summary>
+    public Keyboards Keyboards => _keyboards ??= new Keyboards(this);
+
+    /// <summary>
+    /// Gets the <see cref="IGamepad"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context.
+    /// </summary>
+    public Gamepads Gamepads => _gamepads ??= new Gamepads(this);
+
+    /// <summary>
+    /// Gets the <see cref="IJoystick"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context.
+    /// </summary>
+    public Joysticks Joysticks => _joysticks ??= new Joysticks(this);
+
+    /// <summary>
+    /// Gets the <see cref="IInputDevice"/>s enumerated by the <see cref="IInputBackend"/>s attached to this context.
+    /// </summary>
+    public IReadOnlyList<IInputDevice> Devices
+    {
+        get
+        {
+            if (_devices is not null)
+            {
+                return _devices;
+            }
+
+            foreach (var backend in Backends)
+            {
+                _devices ??= new List<IInputDevice>(backend.Devices.Count);
+                _devices.AddRange(backend.Devices);
+            }
+
+            return _devices ??= [];
+        }
+    }
+
+    /// <summary>
+    /// Gets a list denoting the <see cref="IInputBackend"/> attached to this context.
+    /// </summary>
+    public IList<IInputBackend> Backends => this;
+
+    /// <summary>
+    /// Raised when a device is added or removed from the list of connected <see cref="Devices"/>.
+    /// </summary>
+    public event Action<ConnectionEvent>? ConnectionChanged;
+
+    /// <summary>
+    /// Polls and updates the state of the <see cref="IInputDevice"/> objects connected to each
+    /// <see cref="IInputBackend"/> attached to this context, raising appropriate events for each state change.
+    /// </summary>
+    /// <remarks>
+    /// This calls <see cref="IInputBackend.Update"/> for each <see cref="IInputBackend"/> attached to this context.
+    /// </remarks>
+    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<JoystickButton>.HandleButtonChanged(
+        ButtonChangedEvent<JoystickButton> @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<PointerButton>.HandleButtonChanged(
+        ButtonChangedEvent<PointerButton> @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<KeyName>.HandleButtonChanged(ButtonChangedEvent<KeyName> @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<IInputBackend> IEnumerable<IInputBackend>.GetEnumerator() =>
+        _backends.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => _backends.GetEnumerator();
+
+    void ICollection<IInputBackend>.Add(IInputBackend item)
+    {
+        HandleBackendAddition(item);
+        _backends.Add(item);
+    }
+
+    void ICollection<IInputBackend>.Clear()
+    {
+        foreach (var backend in Backends)
+        {
+            HandleBackendRemoval(backend);
+        }
+    }
+
+    bool ICollection<IInputBackend>.Contains(IInputBackend item) => _backends.Contains(item);
+
+    void ICollection<IInputBackend>.CopyTo(IInputBackend[] array, int arrayIndex) =>
+        _backends.CopyTo(array, arrayIndex);
+
+    bool ICollection<IInputBackend>.Remove(IInputBackend item)
+    {
+        HandleBackendRemoval(item);
+        return _backends.Remove(item);
+    }
+
+    int ICollection<IInputBackend>.Count => _backends.Count;
+
+    bool ICollection<IInputBackend>.IsReadOnly => false;
+
+    int IList<IInputBackend>.IndexOf(IInputBackend item) => _backends.IndexOf(item);
+
+    void IList<IInputBackend>.Insert(int index, IInputBackend item)
+    {
+        HandleBackendAddition(item);
+        _backends.Insert(index, item);
+    }
+
+    void IList<IInputBackend>.RemoveAt(int index)
+    {
+        var backend = _backends[index];
+        HandleBackendRemoval(backend);
+        _backends.RemoveAt(index);
+    }
+
+    IInputBackend IList<IInputBackend>.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;
+
+/// <summary>
+/// An internal class that represents a list of <see cref="InputContext.Devices"/> that are assignable to
+/// <typeparamref name="T"/>. The backing list is lazily initialized.
+/// </summary>
+/// <typeparam name="T">The device type.</typeparam>
+/// <remarks>
+/// This type is not intended for public consumption and has no API/ABI stability guarantees.
+/// </remarks>
+[Experimental(
+    "ST0005",
+    UrlFormat = "https://dotnet.github.io/Silk.NET/docs/v3/silk.net/diagnostics/{0}"
+)]
+public abstract class InputContextDeviceList<T> : IReadOnlyList<T>, IInputHandler
+{
+    private readonly InputContext _ctx;
+    private List<T>? _list;
+
+    internal InputContextDeviceList(InputContext ctx) => _ctx = ctx;
+
+    private List<T> List => _list ??= _ctx.Devices.OfType<T>().ToList();
+
+    /// <inheritdoc />
+    public IEnumerator<T> GetEnumerator() => throw new NotImplementedException();
+
+    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+    /// <inheritdoc />
+    public int Count => List.Count;
+
+    /// <inheritdoc />
+    public T this[int index] => List[index];
+
+    void IInputHandler.HandleDeviceConnectionChanged(ConnectionEvent @event) =>
+        HandleDeviceConnectionChanged(@event);
+
+    /// <inheritdoc cref="IInputHandler.HandleDeviceConnectionChanged"/>
+    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..0d9731e8e7
--- /dev/null
+++ b/sources/Input/Input/InputMarshal.cs
@@ -0,0 +1,636 @@
+// 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;
+
+/// <summary>
+/// Contains utilities for creating and manipulating <see cref="InputReadOnlyList{T}"/>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.
+/// </summary>
+/// <remarks>
+/// 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.
+/// </remarks>
+// NOTE: Not experimental so that we don't eliminate the prospects of third-party implementations.
+public static class InputMarshal
+{
+    /// <summary>
+    /// A wrapper class denoting ownership of a <see cref="InputReadOnlyList{T}"/>. 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.
+    /// </summary>
+    /// <typeparam name="T">The list element type.</typeparam>
+    public struct ListOwner<T>
+    {
+        internal ListOwner(InputReadOnlyList<T> list) => List = list;
+
+        /// <summary>
+        /// Gets the list owned by this owner.
+        /// </summary>
+        public InputReadOnlyList<T> List { get; }
+    }
+
+    internal class ButtonList<T>(uint[] binary, Dictionary<T, Button<T>>? other)
+        : IReadOnlyList<Button<T>>
+        where T : unmanaged, Enum
+    {
+        private Dictionary<T, Button<T>>? _other = other;
+
+        public ButtonList()
+            : this(new uint[(GetButtonListCount<T>() + 32 - 1) / 32], null) { }
+
+        public ButtonList<T> Clone() =>
+            new([.. binary], _other is null ? null : new Dictionary<T, Button<T>>(_other));
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+        public IEnumerator<Button<T>> GetEnumerator() =>
+            typeof(T) == typeof(KeyName) ? GetKeyNameEnumerator() : GetButtonEnumerator();
+
+        private IEnumerator<Button<T>> 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<Button<T>> GetButtonEnumerator()
+        {
+            var max = GetButtonListCount<T>();
+            int idx = 0,
+                bit = 0;
+            for (var i = 1; i <= max; i++)
+            {
+                var ret = ElementAt(SilkMarshal.ConstCast<int, T>(i), idx, bit);
+                (idx, bit) = BitIterate(idx, bit);
+                yield return ret;
+            }
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private Button<T> ElementAt(T name, int idx, int bit)
+        {
+            var ret = new Button<T>(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<T>();
+
+        [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<int, T>(name);
+        }
+
+        [MethodImpl(
+            MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization
+        )]
+        internal static int NameIndex(T name)
+        {
+            var index = SilkMarshal.ConstCast<T, int>(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<T> 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<T> this[T name]
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get
+            {
+                var index = NameIndex(name);
+                if (index >= 0 && index < GetButtonListCount<T>())
+                {
+                    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<T> 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;
+            }
+        }
+    }
+
+    /// <summary>
+    /// Gets the <see cref="IReadOnlyCollection{T}.Count"/> reported by <see cref="InputReadOnlyList{T}"/>s created with
+    /// <see cref="CreateList{T}"/> for the given <typeparamref name="T"/>.
+    /// </summary>
+    /// <typeparam name="T">The button name type.</typeparam>
+    /// <returns>
+    /// The number of buttons that will be in a button list created with <see cref="CreateList{T}"/>, or <c>-1</c> if
+    /// <typeparamref name="T"/> is not a supported button name type.
+    /// </returns>
+    [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+    public static int GetButtonListCount<T>()
+    {
+        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;
+    }
+
+    /// <summary>
+    /// Creates a <see cref="ButtonReadOnlyList{T}"/> wrapping the given button <see cref="InputReadOnlyList{T}"/>.
+    /// </summary>
+    /// <param name="list">The list.</param>
+    /// <typeparam name="T">The button name type.</typeparam>
+    /// <returns>The button list.</returns>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+    public static ButtonReadOnlyList<T> AsButtonList<T>(this InputReadOnlyList<Button<T>> list)
+        where T : unmanaged, Enum => new(list);
+
+    /// <summary>
+    /// Creates a new <see cref="InputReadOnlyList{T}"/> for the given <typeparamref name="T" />, optionally with the
+    /// given <paramref name="capacity"/> where <see cref="GetUnderlyingList{T}"/> is applicable for this
+    /// <typeparamref name="T"/>.
+    /// </summary>
+    /// <param name="capacity">The capacity.</param>
+    /// <typeparam name="T">The element type.</typeparam>
+    /// <returns>The list.</returns>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+    public static ListOwner<T> CreateList<T>(int capacity = 0)
+    {
+        if (typeof(T) == typeof(Button<KeyName>))
+        {
+            return (ListOwner<T>)
+                (object)
+                    new ListOwner<Button<KeyName>>(
+                        new InputReadOnlyList<Button<KeyName>>((object)new ButtonList<KeyName>())
+                    );
+        }
+
+        if (typeof(T) == typeof(Button<JoystickButton>))
+        {
+            return (ListOwner<T>)
+                (object)
+                    new ListOwner<Button<JoystickButton>>(
+                        new InputReadOnlyList<Button<JoystickButton>>(
+                            (object)new ButtonList<JoystickButton>()
+                        )
+                    );
+        }
+
+        if (typeof(T) == typeof(Button<PointerButton>))
+        {
+            return (ListOwner<T>)
+                (object)
+                    new ListOwner<Button<PointerButton>>(
+                        new InputReadOnlyList<Button<PointerButton>>(
+                            (object)new ButtonList<PointerButton>()
+                        )
+                    );
+        }
+
+        return new ListOwner<T>(new InputReadOnlyList<T>((object)new List<T>(capacity)));
+    }
+
+    /// <summary>
+    /// Creates a new <see cref="InputReadOnlyList{T}"/> from the given <see cref="IReadOnlyList{T}"/>. This is
+    /// equivalent to <see cref="InputReadOnlyList{T}(IReadOnlyList{T})"/>, but returns a <see cref="ListOwner{T}"/>
+    /// instead.
+    /// </summary>
+    /// <param name="other">The elements to populate the list with.</param>
+    /// <typeparam name="T"></typeparam>
+    /// <returns></returns>
+    public static ListOwner<T> Clone<T>(IReadOnlyList<T> other)
+    {
+        // ReSharper disable once InvertIf <-- starting to really dislike this as it duplicates code
+        if (other is InputReadOnlyList<T> irl)
+        {
+            if (typeof(T) == typeof(Button<KeyName>))
+            {
+                return new ListOwner<T>(
+                    new InputReadOnlyList<T>(Unsafe.As<ButtonList<KeyName>>(irl.Data).Clone())
+                );
+            }
+
+            if (typeof(T) == typeof(Button<PointerButton>))
+            {
+                return new ListOwner<T>(
+                    new InputReadOnlyList<T>(Unsafe.As<ButtonList<PointerButton>>(irl.Data).Clone())
+                );
+            }
+
+            if (typeof(T) == typeof(Button<JoystickButton>))
+            {
+                return new ListOwner<T>(
+                    new InputReadOnlyList<T>(
+                        Unsafe.As<ButtonList<JoystickButton>>(irl.Data).Clone()
+                    )
+                );
+            }
+        }
+
+        if (typeof(T) == typeof(Button<KeyName>))
+        {
+            return new ListOwner<T>(
+                new InputReadOnlyList<T>(CloneButtonList((IReadOnlyList<Button<KeyName>>)other))
+            );
+        }
+
+        if (typeof(T) == typeof(Button<PointerButton>))
+        {
+            return new ListOwner<T>(
+                new InputReadOnlyList<T>(
+                    CloneButtonList((IReadOnlyList<Button<PointerButton>>)other)
+                )
+            );
+        }
+
+        // ReSharper disable once ConvertIfStatementToReturnStatement <-- stylistic choice
+        if (typeof(T) == typeof(Button<JoystickButton>))
+        {
+            return new ListOwner<T>(
+                new InputReadOnlyList<T>(
+                    CloneButtonList((IReadOnlyList<Button<JoystickButton>>)other)
+                )
+            );
+        }
+
+        return new ListOwner<T>(new InputReadOnlyList<T>((object)new List<T>(other)));
+        static ButtonList<TEnum> CloneButtonList<TEnum>(IReadOnlyList<Button<TEnum>> list)
+            where TEnum : unmanaged, Enum
+        {
+            var ret = new ButtonList<TEnum>();
+            foreach (var button in list)
+            {
+                ret.Set(
+                    button,
+                    (button.IsDown && button.Pressure >= 1.0)
+                        || (!button.IsDown && button.Pressure <= 0.0)
+                );
+            }
+
+            return ret;
+        }
+    }
+
+    /// <summary>
+    /// Sets the button state in the given button list.
+    /// </summary>
+    /// <param name="list">The list to update.</param>
+    /// <param name="value">The new state of the button.</param>
+    /// <param name="isBinary">
+    /// Whether the <see cref="Button{T}.Pressure"/> of <paramref name="value"/> can only be <c>1.0</c> when
+    /// <see cref="Button{T}.IsDown"/> is <c>true</c>, and <c>0.0</c> when <see cref="Button{T}.IsDown"/> is
+    /// <c>false</c>.
+    /// </param>
+    /// <typeparam name="T">The button type.</typeparam>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+    public static void SetButtonState<T>(ListOwner<Button<T>> list, Button<T> value, bool isBinary)
+        where T : unmanaged, Enum
+    {
+        if (
+            typeof(T) == typeof(KeyName)
+            || typeof(T) == typeof(JoystickButton)
+            || typeof(T) == typeof(PointerButton)
+        )
+        {
+            Unsafe.As<ButtonList<T>>(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);
+    }
+
+    /// <summary>
+    /// Attempts to retrieve the underlying <see cref="IList{T}"/> implementation, provided that
+    /// <see cref="InputReadOnlyList{T}"/> for the given <typeparamref name="T"/> is implemented as a sequential list
+    /// with individually addressable and a variable number of  elements.
+    /// </summary>
+    /// <param name="list">The list.</param>
+    /// <typeparam name="T">The list element type.</typeparam>
+    /// <returns>
+    /// The list, or <c>null</c> if the optimised implementation of <see cref="InputReadOnlyList{T}"/> cannot be
+    /// expressed as an <see cref="IList{T}" />.
+    /// </returns>
+    /// <remarks>
+    /// Currently, this can be assumed to not null except for the following types:
+    /// <list type="bullet">
+    /// <item><description><see cref="Button{T}"/> where <c>T</c> is <see cref="KeyName"/></description></item>
+    /// <item><description><see cref="Button{T}"/> where <c>T</c> is <see cref="JoystickButton"/></description></item>
+    /// <item><description><see cref="Button{T}"/> where <c>T</c> is <see cref="PointerButton"/></description></item>
+    /// </list>
+    /// It is a breaking change to change the underlying implementation of the list such that this method returns
+    /// <c>null</c> where it previously did not return <c>null</c>, therefore Silk.NET will only do this in a
+    /// <i>major</i> release. As a result, it is safe to use the <c>!</c> 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.
+    /// </remarks>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)] // <-- generic specialisation
+    public static IList<T>? GetUnderlyingList<T>(this ListOwner<T> list) =>
+        typeof(T) == typeof(Button<KeyName>)
+        || typeof(T) == typeof(Button<PointerButton>)
+        || typeof(T) == typeof(Button<JoystickButton>)
+            ? null
+            : Unsafe.As<List<T>>(list.List.Data);
+
+    // 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<T> GetButtonState<T>(InputReadOnlyList<Button<T>> list, T name)
+        where T : unmanaged, Enum
+    {
+        if (
+            typeof(T) == typeof(KeyName)
+            || typeof(T) == typeof(JoystickButton)
+            || typeof(T) == typeof(PointerButton)
+        )
+        {
+            return Unsafe.As<ButtonList<T>>(list.Data)[name];
+        }
+
+        var underlying = Unsafe.As<List<Button<T>>>(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<T>(name, false, 0);
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    internal static int GetListCount<T>(InputReadOnlyList<T> list)
+    {
+        if (typeof(T) == typeof(Button<KeyName>))
+        {
+            return GetButtonListCount<KeyName>();
+        }
+
+        if (typeof(T) == typeof(Button<PointerButton>))
+        {
+            return GetButtonListCount<PointerButton>();
+        }
+
+        if (typeof(T) == typeof(Button<JoystickButton>))
+        {
+            return GetButtonListCount<JoystickButton>();
+        }
+
+        return Unsafe.As<List<T>>(list.Data).Count;
+    }
+
+    // ReSharper disable NotDisposedResourceIsReturned - Nope, sorry, not adding a reference to JetBrains.Annotations.
+    internal static IEnumerator<T> EnumerateList<T>(InputReadOnlyList<T> list)
+    {
+        if (typeof(T) == typeof(Button<KeyName>))
+        {
+            return (IEnumerator<T>)Unsafe.As<ButtonList<KeyName>>(list.Data).GetEnumerator();
+        }
+
+        if (typeof(T) == typeof(Button<PointerButton>))
+        {
+            return (IEnumerator<T>)Unsafe.As<ButtonList<PointerButton>>(list.Data).GetEnumerator();
+        }
+
+        if (typeof(T) == typeof(Button<JoystickButton>))
+        {
+            return (IEnumerator<T>)Unsafe.As<ButtonList<JoystickButton>>(list.Data).GetEnumerator();
+        }
+
+        return Unsafe.As<List<T>>(list.Data).GetEnumerator();
+    } // ReSharper restore NotDisposedResourceIsReturned
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    internal static T ElementAt<T>(InputReadOnlyList<T> list, int index)
+    {
+        if (typeof(T) == typeof(Button<KeyName>))
+        {
+            return (T)(object)Unsafe.As<ButtonList<KeyName>>(list.Data)[index];
+        }
+
+        if (typeof(T) == typeof(Button<PointerButton>))
+        {
+            return (T)(object)Unsafe.As<ButtonList<PointerButton>>(list.Data)[index];
+        }
+
+        if (typeof(T) == typeof(Button<JoystickButton>))
+        {
+            return (T)(object)Unsafe.As<ButtonList<JoystickButton>>(list.Data)[index];
+        }
+
+        return Unsafe.As<List<T>>(list.Data)[index];
+    }
+}
diff --git a/sources/Input/Input/InputReadOnlyList.cs b/sources/Input/Input/InputReadOnlyList.cs
new file mode 100644
index 0000000000..db3f1546a3
--- /dev/null
+++ b/sources/Input/Input/InputReadOnlyList.cs
@@ -0,0 +1,32 @@
+using System.Collections;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// An opaque implementation of <see cref="IReadOnlyList{T}"/> that is optimised for storing a <c>Silk.NET.Input</c>
+/// type specified by <typeparamref name="T"/> using the most memory-efficient mechanism available.
+/// </summary>
+/// <typeparam name="T">The <c>Silk.NET.Input</c> type to store.</typeparam>
+public readonly struct InputReadOnlyList<T> : IReadOnlyList<T>
+{
+    internal object Data { get; }
+
+    internal InputReadOnlyList(object data) => Data = data;
+
+    /// <summary>
+    /// Creates an <see cref="InputReadOnlyList{T}"/> from a <see cref="IReadOnlyList{T}"/>.
+    /// </summary>
+    /// <param name="other">The list to copy.</param>
+    public InputReadOnlyList(IReadOnlyList<T> other) => this = InputMarshal.Clone(other).List;
+
+    /// <inheritdoc />
+    public IEnumerator<T> GetEnumerator() => InputMarshal.EnumerateList(this);
+
+    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+    /// <inheritdoc />
+    public int Count => InputMarshal.GetListCount(this);
+
+    /// <inheritdoc />
+    public T this[int index] => InputMarshal.ElementAt(this, index);
+}
diff --git a/sources/Input/Input/InputWindowExtensions.cs b/sources/Input/Input/InputWindowExtensions.cs
new file mode 100644
index 0000000000..2933bc7527
--- /dev/null
+++ b/sources/Input/Input/InputWindowExtensions.cs
@@ -0,0 +1,55 @@
+// 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.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains extensions for creating input backends and contexts from <see cref="INativeWindow"/>s.
+/// </summary>
+public static partial class InputWindowExtensions
+{
+    /// <summary>
+    /// Creates an instance of the "reference implementation" of <see cref="IInputBackend"/> for the given
+    /// <see cref="INativeWindow"/>, provided that this was also sourced from the "reference implementation" of the
+    /// windowing API.
+    /// </summary>
+    /// <remarks>
+    /// Regarding the threading rules documented on <see cref="IInputBackend"/>, <see cref="IInputBackend.Update"/>
+    /// must only be called on the "main thread," i.e. the same thread that windowing operates on.
+    /// </remarks>
+    /// <param name="window">The window to create an input backend from.</param>
+    /// <returns>The input backend.</returns>
+    /// <exception cref="NotSupportedException">
+    /// If the given <see cref="INativeWindow"/> is not compatible with the reference implementation for this platform.
+    /// </exception>
+    public static partial IInputBackend CreateInputBackend(this INativeWindow window);
+
+    /// <summary>
+    /// Creates an <see cref="InputContext"/> that uses the "reference implementation" of <see cref="IInputBackend"/>
+    /// for the given <see cref="INativeWindow"/> as its only backend, provided that the <see cref="INativeWindow"/> was
+    /// also sourced from the "reference implementation" of the windowing API.
+    /// </summary>
+    /// <remarks>
+    /// Regarding the threading rules documented on <see cref="InputContext"/>, <see cref="InputContext.Update"/>
+    /// must only be called on the "main thread," i.e. the same thread that windowing operates on.
+    /// </remarks>
+    /// <param name="window">The window to create an input backend from.</param>
+    /// <returns>
+    /// The <see cref="InputContext"/> created with the instantiated input backend as its only backend.
+    /// </returns>
+    /// <exception cref="NotSupportedException">
+    /// If the given <see cref="INativeWindow"/> is not compatible with the reference implementation for this platform.
+    /// </exception>
+    public static InputContext CreateInput(this INativeWindow window)
+    {
+        var ret = new InputContext();
+        ret.Backends.Add(window.CreateInputBackend());
+        return ret;
+    }
+}
diff --git a/sources/Input/Input/JoystickAxisMoveEvent.cs b/sources/Input/Input/JoystickAxisMoveEvent.cs
new file mode 100644
index 0000000000..18e8a1f72c
--- /dev/null
+++ b/sources/Input/Input/JoystickAxisMoveEvent.cs
@@ -0,0 +1,15 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to the movement of a joystick axis.
+/// </summary>
+/// <param name="Joystick">The joystick on which the axis being moved resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Axis">The index of the axis being moved.</param>
+/// <param name="Value">The new value of the axis, typically between <c>0.0</c> and <c>1.0</c>.</param>
+/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param>
+public readonly record struct JoystickAxisMoveEvent(IJoystick Joystick, long Timestamp, int Axis, float Value, float Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/JoystickButton.cs b/sources/Input/Input/JoystickButton.cs
new file mode 100644
index 0000000000..f76482034e
--- /dev/null
+++ b/sources/Input/Input/JoystickButton.cs
@@ -0,0 +1,109 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Enumerates the buttons of a joystick.
+/// </summary>
+public enum JoystickButton
+{
+    /// <summary>
+    /// The button was not recognised.
+    /// </summary>
+    Unknown,
+
+    /// <summary>
+    /// The down-most button of the primary button cluster.
+    /// </summary>
+    ButtonDown,
+
+    /// <summary>
+    /// The "A" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonDown"/>.
+    /// </summary>
+    A = ButtonDown,
+
+    /// <summary>
+    /// The rightmost button of the primary button cluster.
+    /// </summary>
+    ButtonRight,
+
+    /// <summary>
+    /// The "B" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonRight"/>.
+    /// </summary>
+    B = ButtonRight,
+
+    /// <summary>
+    /// The leftmost button of the primary button cluster.
+    /// </summary>
+    ButtonLeft,
+
+    /// <summary>
+    /// The "X" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonLeft"/>.
+    /// </summary>
+    X = ButtonLeft,
+
+    /// <summary>
+    /// The upmost button of the primary button cluster.
+    /// </summary>
+    ButtonUp,
+
+    /// <summary>
+    /// The "Y" button on Xbox (and similar) controllers. Equivalent to <see cref="ButtonUp"/>.
+    /// </summary>
+    Y = ButtonUp,
+
+    /// <summary>
+    /// The leftmost bumper/shoulder button.
+    /// </summary>
+    LeftBumper,
+
+    /// <summary>
+    /// The rightmost bumper/shoulder button.
+    /// </summary>
+    RightBumper,
+
+    /// <summary>
+    /// The "back" button.
+    /// </summary>
+    Back,
+
+    /// <summary>
+    /// The "start" button.
+    /// </summary>
+    Start,
+
+    /// <summary>
+    /// The "home" button.
+    /// </summary>
+    Home,
+
+    /// <summary>
+    /// The leftmost thumbstick. This button represents the stick being pressed down.
+    /// </summary>
+    LeftStick,
+
+    /// <summary>
+    /// The rightmost thumbstick. This button represents the stick being pressed down.
+    /// </summary>
+    RightStick,
+
+    /// <summary>
+    /// The upmost button of the D-Pad button cluster.
+    /// </summary>
+    DPadUp,
+
+    /// <summary>
+    /// The rightmost button of the D-Pad button cluster.
+    /// </summary>
+    DPadRight,
+
+    /// <summary>
+    /// The down-most button of the D-Pad button cluster.
+    /// </summary>
+    DPadDown,
+
+    /// <summary>
+    /// The leftmost button of the D-Pad button cluster.
+    /// </summary>
+    DPadLeft,
+
+    // BEFORE ADDING A NEW ITEM MAKE SURE YOU CHANGE LastJoystickButton IN InputMarshal
+}
diff --git a/sources/Input/Input/JoystickHatMoveEvent.cs b/sources/Input/Input/JoystickHatMoveEvent.cs
new file mode 100644
index 0000000000..67b3defd95
--- /dev/null
+++ b/sources/Input/Input/JoystickHatMoveEvent.cs
@@ -0,0 +1,15 @@
+using System.Diagnostics;
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to the movement of a joystick hat.
+/// </summary>
+/// <param name="Joystick">The joystick on which the hat being moved resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Value">The position of the hat after this event.</param>
+/// <param name="Delta">The change in <see cref="Value"/> as a result of this event.</param>
+public readonly record struct JoystickHatMoveEvent(IJoystick Joystick, long Timestamp, Vector2 Value, Vector2 Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/JoystickState.cs b/sources/Input/Input/JoystickState.cs
new file mode 100644
index 0000000000..9d80287b7b
--- /dev/null
+++ b/sources/Input/Input/JoystickState.cs
@@ -0,0 +1,24 @@
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains user input received from an <see cref="IJoystick"/>.
+/// </summary>
+public class JoystickState
+{
+    /// <summary>
+    /// Gets the state of the joystick axes between <c>-1.0</c> and <c>1.0</c>
+    /// </summary>
+    public InputReadOnlyList<float> Axes { get; }
+
+    /// <summary>
+    /// Gets the joystick button state, denoting which buttons are pressed/depressed.
+    /// </summary>
+    public ButtonReadOnlyList<JoystickButton> Buttons { get; }
+
+    /// <summary>
+    /// Gets the state of the joystick hats as vectors between <c>-1.0</c> and <c>1.0</c>.
+    /// </summary>
+    public InputReadOnlyList<Vector2> Hats { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/Joysticks.cs b/sources/Input/Input/Joysticks.cs
new file mode 100644
index 0000000000..f576e6f85f
--- /dev/null
+++ b/sources/Input/Input/Joysticks.cs
@@ -0,0 +1,41 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Represents a collection of <see cref="IJoystick"/>s from which input events can be received.
+/// </summary>
+public sealed class Joysticks : InputContextDeviceList<IJoystick>, IJoystickInputHandler
+{
+    internal Joysticks(InputContext ctx)
+        : base(ctx) { }
+
+    /// <summary>
+    /// Raised when state pertaining to a pushable button on the joystick changes (e.g. button up, button down).
+    /// </summary>
+    public event Action<ButtonChangedEvent<JoystickButton>>? ButtonChanged;
+
+    /// <summary>
+    /// Raised when a movable axis on the joystick changes position.
+    /// </summary>
+    public event Action<JoystickAxisMoveEvent>? AxisMove;
+
+    /// <summary>
+    /// Raised when a joystick hat moves.
+    /// </summary>
+    public event Action<JoystickHatMoveEvent>? HatMove;
+
+    internal void HandleButtonChanged(ButtonChangedEvent<JoystickButton> @event) =>
+        ButtonChanged?.Invoke(@event);
+
+    void IButtonInputHandler<JoystickButton>.HandleButtonChanged(
+        ButtonChangedEvent<JoystickButton> @event
+    ) => HandleButtonChanged(@event);
+
+    internal void HandleAxisMove(JoystickAxisMoveEvent @event) => AxisMove?.Invoke(@event);
+
+    void IJoystickInputHandler.HandleAxisMove(JoystickAxisMoveEvent @event) =>
+        HandleAxisMove(@event);
+
+    internal void HandleHatMove(JoystickHatMoveEvent @event) => HatMove?.Invoke(@event);
+
+    void IJoystickInputHandler.HandleHatMove(JoystickHatMoveEvent @event) => HandleHatMove(@event);
+}
diff --git a/sources/Input/Input/KeyChangedEvent.cs b/sources/Input/Input/KeyChangedEvent.cs
new file mode 100644
index 0000000000..3868ceaccd
--- /dev/null
+++ b/sources/Input/Input/KeyChangedEvent.cs
@@ -0,0 +1,16 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to a key press state change.
+/// </summary>
+/// <param name="Keyboard">The keyboard on which the key being pressed or depressed resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Key">The new state of the key being pressed or depressed.</param>
+/// <param name="Previous">The previous state of the key.</param>
+/// <param name="IsRepeat">Whether this is an event that has been repeated at an implementation-defined rate.</param>
+/// <param name="Modifiers">The active key modifiers at the time the event was raised.</param>
+public readonly record struct KeyChangedEvent(IKeyboard Keyboard, long Timestamp, Button<KeyName> Key, Button<KeyName> Previous, bool IsRepeat, KeyModifiers Modifiers);
\ No newline at end of file
diff --git a/sources/Input/Input/KeyCharEvent.cs b/sources/Input/Input/KeyCharEvent.cs
new file mode 100644
index 0000000000..8e557d06df
--- /dev/null
+++ b/sources/Input/Input/KeyCharEvent.cs
@@ -0,0 +1,13 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to a character being typed on a keyboard.
+/// </summary>
+/// <param name="Keyboard">The keyboard with which the end user typed a character.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Character">The character that was typed. A <c>null</c> character denotes a backspace.</param>
+public readonly record struct KeyCharEvent(IKeyboard Keyboard, long Timestamp, char? Character);
\ No newline at end of file
diff --git a/sources/Input/Input/KeyModifiers.cs b/sources/Input/Input/KeyModifiers.cs
new file mode 100644
index 0000000000..8b6660641f
--- /dev/null
+++ b/sources/Input/Input/KeyModifiers.cs
@@ -0,0 +1,40 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// A bitmask denoting the modifier keys that can be active when a key press occurs to modify its behaviour.
+/// </summary>
+[Flags]
+public enum KeyModifiers
+{
+    /// <summary>No modifier keys are active.</summary>
+    None = 0,
+    /// <summary>The left "shift" key.</summary>
+    ShiftLeft = 1 << 0,
+
+    /// <summary>The right "shift" key.</summary>
+    ShiftRight = 1 << 1,
+
+    /// <summary>The left "control" key.</summary>
+    ControlLeft = 1 << 2,
+
+    /// <summary>The right "control" key.</summary>
+    ControlRight = 1 << 3,
+
+    /// <summary>The left "alt" key.</summary>
+    AltLeft = 1 << 4,
+
+    /// <summary>The right "alt" key.</summary>
+    AltRight = 1 << 5,
+
+    /// <summary>The left "super" (e.g. Windows/Start) key.</summary>
+    SuperLeft = 1 << 6,
+
+    /// <summary>The right "super" (e.g. Windows/Start) key.</summary>
+    SuperRight = 1 << 7,
+
+    /// <summary>The "num lock" key.</summary>
+    NumLock = 1 << 8,
+
+    /// <summary>The "caps lock" key.</summary>
+    CapsLock = 1 << 9
+}
\ No newline at end of file
diff --git a/sources/Input/Input/KeyName.cs b/sources/Input/Input/KeyName.cs
new file mode 100644
index 0000000000..2def1ba7d7
--- /dev/null
+++ b/sources/Input/Input/KeyName.cs
@@ -0,0 +1,828 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Enumerates names for physical key positions as defined by the
+/// <see href="https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf">USB HID Usage Tables</see> published by
+/// the USB-IF. Note that these denote an en-US-centric definition of the keys that reside at each physical position,
+/// and does not take account of keyboard layout. That is, <see cref="Q"/> represents the <c>Q</c> key on a QWERTY
+/// keyboard but represents the <c>"</c> key on a Dvorak keyboard. Use <see cref="IKeyboard.TryGetKeyName"/> to
+/// determine the localised name of a physical key position name (<see cref="KeyName"/>) when taking account of the
+/// user's selected keyboard layout.
+/// </summary>
+public enum KeyName
+{
+    // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES
+
+    // These values are from usage page 0x07 (USB keyboard page).
+    /// <summary>
+    /// A key that was not recognised.
+    /// </summary>
+    Unknown = 0,
+
+    /// <summary>The "A" key.</summary>
+    A = 4,
+
+    /// <summary>The "B" key.</summary>
+    B = 5,
+
+    /// <summary>The "C" key.</summary>
+    C = 6,
+
+    /// <summary>The "D" key.</summary>
+    D = 7,
+
+    /// <summary>The "E" key.</summary>
+    E = 8,
+
+    /// <summary>The "F" key.</summary>
+    F = 9,
+
+    /// <summary>The "G" key.</summary>
+    G = 10,
+
+    /// <summary>The "H" key.</summary>
+    H = 11,
+
+    /// <summary>The "I" key.</summary>
+    I = 12,
+
+    /// <summary>The "J" key.</summary>
+    J = 13,
+
+    /// <summary>The "K" key.</summary>
+    K = 14,
+
+    /// <summary>The "L" key.</summary>
+    L = 15,
+
+    /// <summary>The "M" key.</summary>
+    M = 16,
+
+    /// <summary>The "N" key.</summary>
+    N = 17,
+
+    /// <summary>The "O" key.</summary>
+    O = 18,
+
+    /// <summary>The "P" key.</summary>
+    P = 19,
+
+    /// <summary>The "Q" key.</summary>
+    Q = 20,
+
+    /// <summary>The "R" key.</summary>
+    R = 21,
+
+    /// <summary>The "S" key.</summary>
+    S = 22,
+
+    /// <summary>The "T" key.</summary>
+    T = 23,
+
+    /// <summary>The "U" key.</summary>
+    U = 24,
+
+    /// <summary>The "V" key.</summary>
+    V = 25,
+
+    /// <summary>The "W" key.</summary>
+    W = 26,
+
+    /// <summary>The "X" key.</summary>
+    X = 27,
+
+    /// <summary>The "Y" key.</summary>
+    Y = 28,
+
+    /// <summary>The "Z" key.</summary>
+    Z = 29,
+
+    /// <summary>The "1" key.</summary>
+    Number1 = 30,
+
+    /// <summary>The "2" key.</summary>
+    Number2 = 31,
+
+    /// <summary>The "3" key.</summary>
+    Number3 = 32,
+
+    /// <summary>The "4" key.</summary>
+    Number4 = 33,
+
+    /// <summary>The "5" key.</summary>
+    Number5 = 34,
+
+    /// <summary>The "6" key.</summary>
+    Number6 = 35,
+
+    /// <summary>The "7" key.</summary>
+    Number7 = 36,
+
+    /// <summary>The "8" key.</summary>
+    Number8 = 37,
+
+    /// <summary>The "9" key.</summary>
+    Number9 = 38,
+
+    /// <summary>The "0" key.</summary>
+    Number0 = 39,
+
+    /// <summary>The "return" key.</summary>
+    Return = 40,
+
+    /// <summary>The "escape" key.</summary>
+    Escape = 41,
+
+    /// <summary>The "backspace" key.</summary>
+    Backspace = 42,
+
+    /// <summary>The "tab" key.</summary>
+    Tab = 43,
+
+    /// <summary>The "space" key.</summary>
+    Space = 44,
+
+    /// <summary>The "minus" key.</summary>
+    Minus = 45,
+
+    /// <summary>The "equals" key.</summary>
+    Equals = 46,
+
+    /// <summary>The "left bracket" key.</summary>
+    LeftBracket = 47,
+
+    /// <summary>The "right bracket" key.</summary>
+    RightBracket = 48,
+
+    /// <summary>The "backslash" key.</summary>
+    Backslash = 49,
+
+    /// <summary>
+    /// A key with region-specific meanings.
+    /// </summary>
+    /// <remarks>
+    /// <list type="bullet">
+    /// <item><term>American</term><description> \|</description></item>
+    /// <item><term>Belgium</term><description> µ`£</description></item>
+    /// <item><term>Canadian-French</term><description> &lt;}&gt;</description></item>
+    /// <item><term>Danish</term><description>’*</description></item>
+    /// <item><term>Dutch</term><description> &lt;&gt;</description></item>
+    /// <item><term>French</term><description>*µ</description></item>
+    /// <item><term>German</term><description> #’</description></item>
+    /// <item><term>Italian</term><description> ù§</description></item>
+    /// <item><term>Latin-American</term><description> }`]</description></item>
+    /// <item><term>Norwegian</term><description>,*</description></item>
+    /// <item><term>Spanish</term><description> }Ç</description></item>
+    /// <item><term>Swedish</term><description> , *</description></item>
+    /// <item><term>Swiss</term><description> $£</description></item>
+    /// <item><term>British</term><description> #~.</description></item>
+    /// </list>
+    /// </remarks>
+    NonUs1 = 50,
+
+    /// <summary>The "semicolon" key.</summary>
+    Semicolon = 51,
+
+    /// <summary>The "apostrophe" key.</summary>
+    Apostrophe = 52,
+
+    /// <summary>The "grave" key.</summary>
+    Grave = 53,
+
+    /// <summary>The "comma" key.</summary>
+    Comma = 54,
+
+    /// <summary>The "period" key.</summary>
+    Period = 55,
+
+    /// <summary>The "slash" key.</summary>
+    Slash = 56,
+
+    /// <summary>The "caps lock" key.</summary>
+    CapsLock = 57,
+
+    /// <summary>The first function key.</summary>
+    F1 = 58,
+
+    /// <summary>The second function key.</summary>
+    F2 = 59,
+
+    /// <summary>The third function key.</summary>
+    F3 = 60,
+
+    /// <summary>The fourth function key.</summary>
+    F4 = 61,
+
+    /// <summary>The fifth function key.</summary>
+    F5 = 62,
+
+    /// <summary>The sixth function key.</summary>
+    F6 = 63,
+
+    /// <summary>The seventh function key.</summary>
+    F7 = 64,
+
+    /// <summary>The eighth function key.</summary>
+    F8 = 65,
+
+    /// <summary>The ninth function key.</summary>
+    F9 = 66,
+
+    /// <summary>The tenth function key.</summary>
+    F10 = 67,
+
+    /// <summary>The eleventh function key.</summary>
+    F11 = 68,
+
+    /// <summary>The twelfth function key.</summary>
+    F12 = 69,
+
+    /// <summary>The "print screen" key.</summary>
+    PrintScreen = 70,
+
+    /// <summary>The "scroll lock" key.</summary>
+    ScrollLock = 71,
+
+    /// <summary>The "pause" key.</summary>
+    Pause = 72,
+
+    /// <summary>The "insert" key.</summary>
+    Insert = 73,
+
+    /// <summary>The "home" key.</summary>
+    Home = 74,
+
+    /// <summary>The "page up" key.</summary>
+    PageUp = 75,
+
+    /// <summary>The "delete" key.</summary>
+    Delete = 76,
+
+    /// <summary>The "end" key.</summary>
+    End = 77,
+
+    /// <summary>The "page down" key.</summary>
+    PageDown = 78,
+
+    /// <summary>The "right" key.</summary>
+    Right = 79,
+
+    /// <summary>The "left" key.</summary>
+    Left = 80,
+
+    /// <summary>The "down" key.</summary>
+    Down = 81,
+
+    /// <summary>The "up" key.</summary>
+    Up = 82,
+
+    /// <summary>The "num lock clear" key.</summary>
+    NumLockClear = 83,
+
+    /// <summary>The "divide" key on the keypad.</summary>
+    KeypadDivide = 84,
+
+    /// <summary>The "multiply" key on the keypad.</summary>
+    KeypadMultiply = 85,
+
+    /// <summary>The "minus" key on the keypad.</summary>
+    KeypadMinus = 86,
+
+    /// <summary>The "plus" key on the keypad.</summary>
+    KeypadPlus = 87,
+
+    /// <summary>The "enter" key on the keypad.</summary>
+    KeypadEnter = 88,
+
+    /// <summary>The "1" key on the keypad.</summary>
+    Keypad1 = 89,
+
+    /// <summary>The "2" key on the keypad.</summary>
+    Keypad2 = 90,
+
+    /// <summary>The "3" key on the keypad.</summary>
+    Keypad3 = 91,
+
+    /// <summary>The "4" key on the keypad.</summary>
+    Keypad4 = 92,
+
+    /// <summary>The "5" key on the keypad.</summary>
+    Keypad5 = 93,
+
+    /// <summary>The "6" key on the keypad.</summary>
+    Keypad6 = 94,
+
+    /// <summary>The "7" key on the keypad.</summary>
+    Keypad7 = 95,
+
+    /// <summary>The "8" key on the keypad.</summary>
+    Keypad8 = 96,
+
+    /// <summary>The "9" key on the keypad.</summary>
+    Keypad9 = 97,
+
+    /// <summary>The "0" key on the keypad.</summary>
+    Keypad0 = 98,
+
+    /// <summary>The "period" key on the keypad.</summary>
+    KeypadPeriod = 99,
+
+    /// <summary>
+    /// A key with region-specific meanings, typically near the Left-Shift key in AT-102 implementations.
+    /// </summary>
+    /// <remarks>
+    /// <item><term>Belg</term><description>&lt;\&gt;</description></item>
+    /// <item><term>FrCa</term><description>«°»</description></item>
+    /// <item><term>Dan</term><description>&lt;\&gt;</description></item>
+    /// <item><term>Dutch</term><description>]|[</description></item>
+    /// <item><term>Fren</term><description>&lt;&gt;</description></item>
+    /// <item><term>Ger</term><description>&lt;|&gt;</description></item>
+    /// <item><term>Ital</term><description>&lt;&gt;</description></item>
+    /// <item><term>LatAm</term><description>&lt;&gt;</description></item>
+    /// <item><term>Nor</term><description>&lt;&gt;</description></item>
+    /// <item><term>Span</term><description>&lt;&gt;</description></item>
+    /// <item><term>Swed</term><description>&lt;|&gt;</description></item>
+    /// <item><term>Swiss</term><description>&lt;\&gt;</description></item>
+    /// <item><term>UK</term><description>\|</description></item>
+    /// <item><term>Brazil</term><description>\|</description></item>
+    /// </remarks>
+    NonUs2 = 100,
+
+    /// <summary>A key for application-defined functions.</summary>
+    Application = 101,
+
+    /// <summary>The "power" key.</summary>
+    Power = 102,
+
+    /// <summary>The "equals" key on the keypad.</summary>
+    KeypadEquals = 103,
+
+    /// <summary>The thirteenth function key.</summary>
+    F13 = 104,
+
+    /// <summary>The fourteenth function key.</summary>
+    F14 = 105,
+
+    /// <summary>The fifteenth function key.</summary>
+    F15 = 106,
+
+    /// <summary>The sixteenth function key.</summary>
+    F16 = 107,
+
+    /// <summary>The seventeenth function key.</summary>
+    F17 = 108,
+
+    /// <summary>The eighteenth function key.</summary>
+    F18 = 109,
+
+    /// <summary>The nineteenth function key.</summary>
+    F19 = 110,
+
+    /// <summary>The twentieth function key.</summary>
+    F20 = 111,
+
+    /// <summary>The twenty-first function key.</summary>
+    F21 = 112,
+
+    /// <summary>The twenty-second function key.</summary>
+    F22 = 113,
+
+    /// <summary>The twenty-third function key.</summary>
+    F23 = 114,
+
+    /// <summary>The twenty-fourth function key.</summary>
+    F24 = 115,
+
+    /// <summary>The "execute" key.</summary>
+    Execute = 116,
+
+    /// <summary>The "help" key.</summary>
+    Help = 117,
+
+    /// <summary>The "menu" key.</summary>
+    Menu = 118,
+
+    /// <summary>The "select" key.</summary>
+    Select = 119,
+
+    /// <summary>The "stop" key.</summary>
+    Stop = 120,
+
+    /// <summary>The "again" key.</summary>
+    Again = 121,
+
+    /// <summary>The "undo" key.</summary>
+    Undo = 122,
+
+    /// <summary>The "cut" key.</summary>
+    Cut = 123,
+
+    /// <summary>The "copy" key.</summary>
+    Copy = 124,
+
+    /// <summary>The "paste" key.</summary>
+    Paste = 125,
+
+    /// <summary>The "find" key.</summary>
+    Find = 126,
+
+    /// <summary>The "mute" key.</summary>
+    Mute = 127,
+
+    /// <summary>The "volume up" key.</summary>
+    VolumeUp = 128,
+
+    /// <summary>The "volume down" key.</summary>
+    VolumeDown = 129,
+
+    // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES
+
+    /// <summary>The "comma" key on the keypad.</summary>
+    KeypadComma = 133,
+
+    /// <summary>The alternative "equals" key on the keypad as typically found on AS-400 keyboards.</summary>
+    OtherKeypadEquals = 134,
+
+    /// <summary>The first international key.</summary>
+    International1 = 135,
+
+    /// <summary>The second international key.</summary>
+    International2 = 136,
+
+    /// <summary>The third international key.</summary>
+    International3 = 137,
+
+    /// <summary>The fourth international key.</summary>
+    International4 = 138,
+
+    /// <summary>The fifth international key.</summary>
+    International5 = 139,
+
+    /// <summary>The sixth international key.</summary>
+    International6 = 140,
+
+    /// <summary>The seventh international key.</summary>
+    International7 = 141,
+
+    /// <summary>The eighth international key.</summary>
+    International8 = 142,
+
+    /// <summary>The ninth international key.</summary>
+    International9 = 143,
+
+    /// <summary>The first language key.</summary>
+    Lang1 = 144,
+
+    /// <summary>The second language key.</summary>
+    Lang2 = 145,
+
+    /// <summary>The third language key.</summary>
+    Lang3 = 146,
+
+    /// <summary>The fourth language key.</summary>
+    Lang4 = 147,
+
+    /// <summary>The fifth language key.</summary>
+    Lang5 = 148,
+
+    /// <summary>The sixth language key.</summary>
+    Lang6 = 149,
+
+    /// <summary>The seventh language key.</summary>
+    Lang7 = 150,
+
+    /// <summary>The eighth language key.</summary>
+    Lang8 = 151,
+
+    /// <summary>The ninth language key.</summary>
+    Lang9 = 152,
+
+    /// <summary>The alternative "erase" key, for example an Erase-Eaze™ key.</summary>
+    AlternativeErase = 153,
+
+    /// <summary>The "system request" key.</summary>
+    SystemRequest = 154,
+
+    /// <summary>The "cancel" key.</summary>
+    Cancel = 155,
+
+    /// <summary>The "clear" key.</summary>
+    Clear = 156,
+
+    /// <summary>The "prior" key.</summary>
+    Prior = 157,
+
+    /// <summary>An alternative "return" key.</summary>
+    Return2 = 158,
+
+    /// <summary>The "separator" key.</summary>
+    Separator = 159,
+
+    /// <summary>The "out" key.</summary>
+    Out = 160,
+
+    /// <summary>The "operation" key.</summary>
+    Oper = 161,
+
+    /// <summary>The "clear again" key.</summary>
+    ClearAgain = 162,
+
+    /// <summary>The "cursor select" key.</summary>
+    /// <remarks>
+    /// For more information consult IBM's "3174 Establishment Controller - Terminal User's Reference for Expanded
+    /// Functions" (GA23-03320-02, May 1989)
+    /// </remarks>
+    CursorSelect = 163,
+
+    /// <summary>The "extend select" key.</summary>
+    /// <remarks>
+    /// For more information consult IBM's "3174 Establishment Controller - Terminal User's Reference for Expanded
+    /// Functions" (GA23-03320-02, May 1989)
+    /// </remarks>
+    ExtendSelect = 164,
+
+    // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES
+
+    /// <summary>The "00" key on the keypad.</summary>
+    Keypad00 = 176,
+
+    /// <summary>The "000" key on the keypad.</summary>
+    Keypad000 = 177,
+
+    /// <summary>The "thousands separator" key.</summary>
+    /// <remarks>Interpreted as a comma for en-US.</remarks>
+    ThousandsSeparator = 178,
+
+    /// <summary>The "decimal separator" key.</summary>
+    /// <remarks>Interpreted as a period for en-US.</remarks>
+    DecimalSeparator = 179,
+
+    /// <summary>The "currency unit" key.</summary>
+    /// <remarks>Interpreted as a dollar sign for en-US.</remarks>
+    CurrencyUnit = 180,
+
+    /// <summary>The "currencySubunit" key.</summary>
+    /// <remarks>Interpreted as a cents symbol for en-US.</remarks>
+    CurrencySubunit = 181,
+
+    /// <summary>The "leftParenthesis" key on the keypad.</summary>
+    KeypadLeftParenthesis = 182,
+
+    /// <summary>The "rightParenthesis" key on the keypad.</summary>
+    KeypadRightParenthesis = 183,
+
+    /// <summary>The "leftBrace" key on the keypad.</summary>
+    KeypadLeftBrace = 184,
+
+    /// <summary>The "rightBrace" key on the keypad.</summary>
+    KeypadRightBrace = 185,
+
+    /// <summary>The "tab" key on the keypad.</summary>
+    KeypadTab = 186,
+
+    /// <summary>The "backspace" key on the keypad.</summary>
+    KeypadBackspace = 187,
+
+    /// <summary>The "a" key on the keypad.</summary>
+    KeypadA = 188,
+
+    /// <summary>The "b" key on the keypad.</summary>
+    KeypadB = 189,
+
+    /// <summary>The "c" key on the keypad.</summary>
+    KeypadC = 190,
+
+    /// <summary>The "d" key on the keypad.</summary>
+    KeypadD = 191,
+
+    /// <summary>The "e" key on the keypad.</summary>
+    KeypadE = 192,
+
+    /// <summary>The "f" key on the keypad.</summary>
+    KeypadF = 193,
+
+    /// <summary>The "xor" key on the keypad.</summary>
+    KeypadXor = 194,
+
+    /// <summary>The "power" key on the keypad.</summary>
+    KeypadPower = 195,
+
+    /// <summary>The "percent" key on the keypad.</summary>
+    KeypadPercent = 196,
+
+    /// <summary>The "less" key on the keypad.</summary>
+    KeypadLess = 197,
+
+    /// <summary>The "greater" key on the keypad.</summary>
+    KeypadGreater = 198,
+
+    /// <summary>The "ampersand" key on the keypad.</summary>
+    KeypadAmpersand = 199,
+
+    /// <summary>The "doubleAmpersand" key on the keypad.</summary>
+    KeypadDoubleAmpersand = 200,
+
+    /// <summary>The "vertical bar" key on the keypad.</summary>
+    KeypadVerticalBar = 201,
+
+    /// <summary>The "double vertical bar" key on the keypad.</summary>
+    KeypadDoubleVerticalBar = 202,
+
+    /// <summary>The "colon" key on the keypad.</summary>
+    KeypadColon = 203,
+
+    /// <summary>The "hash" key on the keypad.</summary>
+    KeypadHash = 204,
+
+    /// <summary>The "space" key on the keypad.</summary>
+    KeypadSpace = 205,
+
+    /// <summary>The "@" key on the keypad.</summary>
+    KeypadAt = 206,
+
+    /// <summary>The "exclamation" key on the keypad.</summary>
+    KeypadExclamation = 207,
+
+    /// <summary>The "memory store" key on the keypad.</summary>
+    KeypadMemoryStore = 208,
+
+    /// <summary>The "memory recall" key on the keypad.</summary>
+    KeypadMemoryRecall = 209,
+
+    /// <summary>The "memory clear" key on the keypad.</summary>
+    KeypadMemoryClear = 210,
+
+    /// <summary>The "memory add" key on the keypad.</summary>
+    KeypadMemoryAdd = 211,
+
+    /// <summary>The "memory subtract" key on the keypad.</summary>
+    KeypadMemorySubtract = 212,
+
+    /// <summary>The "memory multiply" key on the keypad.</summary>
+    KeypadMemoryMultiply = 213,
+
+    /// <summary>The "memory divide" key on the keypad.</summary>
+    KeypadMemoryDivide = 214,
+
+    /// <summary>The "plus/minus" key on the keypad.</summary>
+    KeypadPlusMinus = 215,
+
+    /// <summary>The "clear" key on the keypad.</summary>
+    KeypadClear = 216,
+
+    /// <summary>The "clear entry" key on the keypad.</summary>
+    KeypadClearEntry = 217,
+
+    /// <summary>The "binary" key on the keypad.</summary>
+    KeypadBinary = 218,
+
+    /// <summary>The "octal" key on the keypad.</summary>
+    KeypadOctal = 219,
+
+    /// <summary>The "decimal" key on the keypad.</summary>
+    KeypadDecimal = 220,
+
+    /// <summary>The "hexadecimal" key on the keypad.</summary>
+    KeypadHexadecimal = 221,
+
+    // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES
+
+    /// <summary>The left "control" key.</summary>
+    ControlLeft = 224,
+
+    /// <summary>The left "shift" key.</summary>
+    ShiftLeft = 225,
+
+    /// <summary>The left "alt" key.</summary>
+    AltLeft = 226,
+
+    /// <summary>The left "super" (e.g. Windows/Start) key.</summary>
+    SuperLeft = 227,
+
+    /// <summary>The right "control" key.</summary>
+    ControlRight = 228,
+
+    /// <summary>The right "shift" key.</summary>
+    ShiftRight = 229,
+
+    /// <summary>The right "alt" key.</summary>
+    AltRight = 230,
+
+    /// <summary>The right "super" (e.g. Windows/Start) key.</summary>
+    SuperRight = 231,
+
+    // 232-256..... wtf?
+    // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES
+
+    /// <summary>The "mode" key.</summary>
+    Mode = 257,
+
+    // These values are mapped from usage page 0x0C (USB consumer page).
+    /// <summary>The "sleep" key.</summary>
+    Sleep = 258,
+
+    /// <summary>The "wake" key.</summary>
+    Wake = 259,
+
+    /// <summary>The "channel increment" key.</summary>
+    ChannelIncrement = 260,
+
+    /// <summary>The "channel decrement" key.</summary>
+    ChannelDecrement = 261,
+
+    /// <summary>The "play" media key.</summary>
+    MediaPlay = 262,
+
+    /// <summary>The "pause" media key.</summary>
+    MediaPause = 263,
+
+    /// <summary>The "record" media key.</summary>
+    MediaRecord = 264,
+
+    /// <summary>The "fast forward" media key.</summary>
+    MediaFastForward = 265,
+
+    /// <summary>The "rewind" media key.</summary>
+    MediaRewind = 266,
+
+    /// <summary>The "next track" media key.</summary>
+    MediaNextTrack = 267,
+
+    /// <summary>The "previous track" media key.</summary>
+    MediaPreviousTrack = 268,
+
+    /// <summary>The "stop" media key.</summary>
+    MediaStop = 269,
+
+    /// <summary>The "eject" media key.</summary>
+    MediaEject = 270,
+
+    /// <summary>The "play/pause" media key.</summary>
+    MediaPlayPause = 271,
+
+    /// <summary>The "select" media key.</summary>
+    MediaSelect = 272,
+
+    /// <summary>The "new" application key.</summary>
+    ApplicationNew = 273,
+
+    /// <summary>The "open" application key.</summary>
+    ApplicationOpen = 274,
+
+    /// <summary>The "close" application key.</summary>
+    ApplicationClose = 275,
+
+    /// <summary>The "exit" application key.</summary>
+    ApplicationExit = 276,
+
+    /// <summary>The "save" application key.</summary>
+    ApplicationSave = 277,
+
+    /// <summary>The "print" application key.</summary>
+    ApplicationPrint = 278,
+
+    /// <summary>The "properties" application key.</summary>
+    ApplicationProperties = 279,
+
+    /// <summary>The "search" application key.</summary>
+    ApplicationSearch = 280,
+
+    /// <summary>The "home" application key.</summary>
+    ApplicationHome = 281,
+
+    /// <summary>The "back" application key.</summary>
+    ApplicationBack = 282,
+
+    /// <summary>The "forward" application key.</summary>
+    ApplicationForward = 283,
+
+    /// <summary>The "stop" application key.</summary>
+    ApplicationStop = 284,
+
+    /// <summary>The "refresh" application key.</summary>
+    ApplicationRefresh = 285,
+
+    /// <summary>The "bookmarks" application key.</summary>
+    ApplicationBookmarks = 286,
+
+    // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES
+
+    // 501-512 is reserved for non-standard (i.e. not from an industry-standard HID page) keys.
+    /// <summary>The left soft key e.g. the left button on a mobile phone.</summary>
+    /// <remarks>This is not from an industry-standard HID page.</remarks>
+    SoftLeft = 501,
+
+    /// <summary>The right soft key e.g. the right button on a mobile phone.</summary>
+    /// <remarks>This is not from an industry-standard HID page.</remarks>
+    SoftRight = 502,
+
+    /// <summary>The "call" key.</summary>
+    /// <remarks>This is not from an industry-standard HID page.</remarks>
+    Call = 503,
+
+    /// <summary>The "end call" key.</summary>
+    /// <remarks>This is not from an industry-standard HID page.</remarks>
+    EndCall = 504,
+
+    // BEFORE ADDING ANYTHING TO THIS FILE MAKE SURE YOU REALISE THAT InputMarshal RELIES ON ASSUMPTIONS ON THE VALUES
+}
diff --git a/sources/Input/Input/KeyboardState.cs b/sources/Input/Input/KeyboardState.cs
new file mode 100644
index 0000000000..a9e28ac478
--- /dev/null
+++ b/sources/Input/Input/KeyboardState.cs
@@ -0,0 +1,23 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains user input received from an <see cref="IKeyboard"/>.
+/// </summary>
+public class KeyboardState
+{
+    /// <summary>
+    /// Gets the text that has been typed since <see cref="IKeyboard.BeginInput"/> has been called. This will be cleared
+    /// when <see cref="IKeyboard.EndInput"/> is called.
+    /// </summary>
+    public InputReadOnlyList<char>? Text { get; }
+
+    /// <summary>
+    /// Gets the key state, denoting which keys are pressed on the keyboard.
+    /// </summary>
+    public ButtonReadOnlyList<KeyName> Keys { get; }
+
+    /// <summary>
+    /// Gets the active modifier keys.
+    /// </summary>
+    public KeyModifiers Modifiers { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/Keyboards.cs b/sources/Input/Input/Keyboards.cs
new file mode 100644
index 0000000000..34c59a25c6
--- /dev/null
+++ b/sources/Input/Input/Keyboards.cs
@@ -0,0 +1,33 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Represents a collection of <see cref="IKeyboard"/>s from which input events can be received.
+/// </summary>
+public sealed class Keyboards : InputContextDeviceList<IKeyboard>, IKeyboardInputHandler
+{
+    internal Keyboards(InputContext ctx)
+        : base(ctx) { }
+
+    /// <summary>
+    /// Raised when state pertaining to a pushable key on the keyboard changes (e.g. key up, key down, key repeat).
+    /// </summary>
+    public event Action<KeyChangedEvent>? KeyChanged;
+
+    /// <summary>
+    /// Raised when the user types a character using the keyboard.
+    /// </summary>
+    public event Action<KeyCharEvent>? KeyChar;
+
+    internal void HandleButtonChanged(ButtonChangedEvent<KeyName> @event) { }
+
+    void IButtonInputHandler<KeyName>.HandleButtonChanged(ButtonChangedEvent<KeyName> @event) =>
+        HandleButtonChanged(@event);
+
+    internal void HandleKeyChanged(KeyChangedEvent @event) => KeyChanged?.Invoke(@event);
+
+    void IKeyboardInputHandler.HandleKeyChanged(KeyChangedEvent @event) => HandleKeyChanged(@event);
+
+    internal void HandleKeyChar(KeyCharEvent @event) => KeyChar?.Invoke(@event);
+
+    void IKeyboardInputHandler.HandleKeyChar(KeyCharEvent @event) => HandleKeyChar(@event);
+}
diff --git a/sources/Input/Input/MouseScrollEvent.cs b/sources/Input/Input/MouseScrollEvent.cs
new file mode 100644
index 0000000000..737950479e
--- /dev/null
+++ b/sources/Input/Input/MouseScrollEvent.cs
@@ -0,0 +1,18 @@
+using System.Diagnostics;
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to the user scrolling using a mouse scroll wheel.
+/// </summary>
+/// <param name="Mouse">The mouse on which the scroll wheel resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Point">The mouse's active point when the scroll event occurred.</param>
+/// <param name="WheelPosition">The <see cref="MouseState.WheelPosition"/> after the event occurred.</param>
+/// <param name="Delta">
+/// The change in <see cref="WheelPosition"/> as a result of this event represented as a number of ratchets.
+/// </param>
+public readonly record struct MouseScrollEvent(IMouse Mouse, long Timestamp, TargetPoint Point, Vector2 WheelPosition, Vector2 Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/MouseState.cs b/sources/Input/Input/MouseState.cs
new file mode 100644
index 0000000000..fe6a776b0e
--- /dev/null
+++ b/sources/Input/Input/MouseState.cs
@@ -0,0 +1,14 @@
+using System.Numerics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains user input received from an <see cref="IMouse"/>.
+/// </summary>
+public class MouseState : PointerState
+{
+    /// <summary>
+    /// Gets the current position of the scroll wheel in number of ratchets.
+    /// </summary>
+    public Vector2 WheelPosition { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/PointChangedEvent.cs b/sources/Input/Input/PointChangedEvent.cs
new file mode 100644
index 0000000000..cf9ae15e8c
--- /dev/null
+++ b/sources/Input/Input/PointChangedEvent.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to a <see cref="TargetPoint"/> change on a <see cref="IPointerDevice"/>,
+/// </summary>
+/// <param name="Pointer">The pointer device with which the user is pointing.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="OldPoint">
+/// The previous state for this <see cref="TargetPoint"/>. If this is a new point (e.g. a finger has only just touched a
+/// touch screen), this shall be <c>null</c>.
+/// </param>
+/// <param name="NewPoint">
+/// The new state for this <see cref="TargetPoint"/>. If the point is no longer valid (e.g. a finger is no longer
+/// touching a touch screen), this shall be <c>null</c>.
+/// </param>
+public readonly record struct PointChangedEvent(
+    IPointerDevice Pointer,
+    long Timestamp,
+    TargetPoint? OldPoint,
+    TargetPoint? NewPoint
+);
diff --git a/sources/Input/Input/PointerButton.cs b/sources/Input/Input/PointerButton.cs
new file mode 100644
index 0000000000..f0874e646e
--- /dev/null
+++ b/sources/Input/Input/PointerButton.cs
@@ -0,0 +1,184 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Enumerates the buttons available on pointer devices.
+/// </summary>
+public enum PointerButton
+{
+    /// <summary>
+    /// An unrecognised button.
+    /// </summary>
+    Unknown,
+
+    /// <summary>
+    /// The primary button e.g. left click.
+    /// </summary>
+    Primary,
+
+    /// <summary>
+    /// The secondary button e.g. right click.
+    /// </summary>
+    Secondary,
+
+    /// <summary>
+    /// The third button.
+    /// </summary>
+    Button3,
+
+    /// <summary>
+    /// The middle button i.e. clicking the scroll wheel down. This acts as the third button.
+    /// </summary>
+    MiddleButton = Button3,
+
+    /// <summary>
+    /// The fourth button.
+    /// </summary>
+    Button4,
+
+    /// <summary>
+    /// The fifth button.
+    /// </summary>
+    Button5,
+
+    /// <summary>
+    /// The sixth button.
+    /// </summary>
+    Button6,
+
+    /// <summary>
+    /// The seventh button.
+    /// </summary>
+    Button7,
+
+    /// <summary>
+    /// The eighth button.
+    /// </summary>
+    Button8,
+
+    /// <summary>
+    /// The ninth button.
+    /// </summary>
+    Button9,
+
+    /// <summary>
+    /// The tenth button.
+    /// </summary>
+    Button10,
+
+    /// <summary>
+    /// The eleventh button.
+    /// </summary>
+    Button11,
+
+    /// <summary>
+    /// The twelveth button.
+    /// </summary>
+    Button12,
+
+    /// <summary>
+    /// The thirteenth button.
+    /// </summary>
+    Button13,
+
+    /// <summary>
+    /// The fourteenth button.
+    /// </summary>
+    Button14,
+
+    /// <summary>
+    /// The fifteenth button.
+    /// </summary>
+    Button15,
+
+    /// <summary>
+    /// The sixteenth button.
+    /// </summary>
+    Button16,
+
+    /// <summary>
+    /// The seventeenth button.
+    /// </summary>
+    Button17,
+
+    /// <summary>
+    /// The eighteenth button.
+    /// </summary>
+    Button18,
+
+    /// <summary>
+    /// The nineteenth button.
+    /// </summary>
+    Button19,
+
+    /// <summary>
+    /// The twentieth button.
+    /// </summary>
+    Button20,
+
+    /// <summary>
+    /// The twenty-first button.
+    /// </summary>
+    Button21,
+
+    /// <summary>
+    /// The twenty-second button.
+    /// </summary>
+    Button22,
+
+    /// <summary>
+    /// The twenty-third button.
+    /// </summary>
+    Button23,
+
+    /// <summary>
+    /// The twenty-fourth button.
+    /// </summary>
+    Button24,
+
+    /// <summary>
+    /// The twenty-fifth button.
+    /// </summary>
+    Button25,
+
+    /// <summary>
+    /// The twenty-sixth button.
+    /// </summary>
+    Button26,
+
+    /// <summary>
+    /// The twenty-seventh button.
+    /// </summary>
+    Button27,
+
+    /// <summary>
+    /// The twenty-eighth button.
+    /// </summary>
+    Button28,
+
+    /// <summary>
+    /// The twenty-ninth button.
+    /// </summary>
+    Button29,
+
+    /// <summary>
+    /// The thirtieth button.
+    /// </summary>
+    Button30,
+
+    /// <summary>
+    /// The eraser tip of a pen pointer device. This acts as the thirtieth button.
+    /// </summary>
+    EraserTip = Button30,
+
+    /// <summary>
+    /// The thirty-first button.
+    /// </summary>
+    Button31,
+
+    /// <summary>
+    /// The thirty-second button.
+    /// </summary>
+    Button32,
+
+    // BEFORE ADDING MORE BUTTONS, ENSURE YOU CHANGE InputMarshal TO ACCOUNT FOR THE NEW MAX
+}
diff --git a/sources/Input/Input/PointerClickConfiguration.cs b/sources/Input/Input/PointerClickConfiguration.cs
new file mode 100644
index 0000000000..4612da0780
--- /dev/null
+++ b/sources/Input/Input/PointerClickConfiguration.cs
@@ -0,0 +1,19 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Denotes the configuration for recognising <see cref="Pointers.DoubleClick"/> events apart from single
+/// <see cref="Pointers.Click"/> events.
+/// </summary>
+/// <param name="DoubleClickTime">
+/// The maximum time in milliseconds between two consecutive clicks to count as a double click.
+/// </param>
+/// <param name="DoubleClickRange">
+/// The maximum distance in pixels between two consecutive clicks to count as a double click.
+/// </param>
+public record struct PointerClickConfiguration(int DoubleClickTime, float DoubleClickRange)
+{
+    /// <summary>
+    /// Gets the default configuration.
+    /// </summary>
+    public static PointerClickConfiguration Default => new(500, 4);
+}
diff --git a/sources/Input/Input/PointerClickEvent.cs b/sources/Input/Input/PointerClickEvent.cs
new file mode 100644
index 0000000000..b5c75eece6
--- /dev/null
+++ b/sources/Input/Input/PointerClickEvent.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to a pointer button being pressed and released (i.e. clicked).
+/// </summary>
+/// <param name="Pointer">The pointer device on which the button being pressed and released resides.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Point">
+/// A specific <see cref="TargetPoint"/> for which the button press occurred, check <see cref="TargetPoint.IsValid"/> to
+/// validate if such a point was available.
+/// </param>
+/// <param name="Button">The button that was pressed and released in succession.</param>
+public readonly record struct PointerClickEvent(IPointerDevice Pointer, long Timestamp, TargetPoint Point, PointerButton Button);
\ No newline at end of file
diff --git a/sources/Input/Input/PointerGripChangedEvent.cs b/sources/Input/Input/PointerGripChangedEvent.cs
new file mode 100644
index 0000000000..079e1e4e8a
--- /dev/null
+++ b/sources/Input/Input/PointerGripChangedEvent.cs
@@ -0,0 +1,18 @@
+using System.Diagnostics;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to the user changing the pressure with which they're applying their grip on the
+/// given pointer device.
+/// </summary>
+/// <param name="Pointer">The pointer device the user is gripping.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="GripPressure">
+/// The grip pressure being applied to the device, where <c>0.0</c> is the lowest amount of pressure measurable by the
+/// device and <c>1.0</c> is the maximum amount of pressure measurable by the device.
+/// </param>
+/// <param name="Delta">The change in <see cref="GripPressure"/> from its previous value.</param>
+public readonly record struct PointerGripChangedEvent(IPointerDevice Pointer, long Timestamp, float GripPressure, float Delta);
\ No newline at end of file
diff --git a/sources/Input/Input/PointerState.cs b/sources/Input/Input/PointerState.cs
new file mode 100644
index 0000000000..b69372214c
--- /dev/null
+++ b/sources/Input/Input/PointerState.cs
@@ -0,0 +1,23 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains user input state received from an <see cref="IPointerDevice"/>.
+/// </summary>
+public class PointerState
+{
+    /// <summary>
+    /// Gets the captured state of each of the buttons on the device.
+    /// </summary>
+    public ButtonReadOnlyList<PointerButton> Buttons { get; }
+
+    /// <summary>
+    /// Gets the points on the targets at which the user is pointing using the device.
+    /// </summary>
+    public InputReadOnlyList<TargetPoint> Points { get; }
+
+    /// <summary>
+    /// Gets the pressure the user is applying to the grip of the pointer device, where <c>0.0</c> is the lowest
+    /// measurable pressure and <c>1.0</c> is the highest measurable pressure.
+    /// </summary>
+    public float GripPressure { get; }
+}
\ No newline at end of file
diff --git a/sources/Input/Input/PointerTargetChangedEvent.cs b/sources/Input/Input/PointerTargetChangedEvent.cs
new file mode 100644
index 0000000000..49a97e5950
--- /dev/null
+++ b/sources/Input/Input/PointerTargetChangedEvent.cs
@@ -0,0 +1,27 @@
+using System.Diagnostics;
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Contains information pertaining to changes to a "target" at which the user can point using a pointer device.
+/// </summary>
+/// <param name="Pointer">The pointer with which the user can point at the given target.</param>
+/// <param name="Timestamp">
+/// The timestamp (as retrieved from <see cref="Stopwatch.GetTimestamp"/>) at which the event occurred.
+/// </param>
+/// <param name="Target">The target at which the user can point.</param>
+/// <param name="IsAdded">
+/// <c>true</c> if this is a newly-added target to <see cref="IPointerDevice.Targets"/>,
+/// <c>false</c> if this target has been removed from the list of available <see cref="IPointerDevice.Targets"/>,
+/// <c>null</c> if there has been no change to the target's validity.
+/// </param>
+/// <param name="OldBounds">
+/// The old <see cref="IPointerTarget.Bounds"/> of the target. This may be the same as <see cref="NewBounds"/> if there
+/// has been no change.
+/// </param>
+/// <param name="NewBounds">
+/// The new <see cref="IPointerTarget.Bounds"/> of the target. This may be the same as <see cref="OldBounds"/> if there
+/// has been no change.
+/// </param>
+public readonly record struct PointerTargetChangedEvent(IPointerDevice Pointer, long Timestamp, IPointerTarget Target, bool? IsAdded, Box3D<float> OldBounds, Box3D<float> NewBounds);
\ No newline at end of file
diff --git a/sources/Input/Input/Pointers.cs b/sources/Input/Input/Pointers.cs
new file mode 100644
index 0000000000..f1d5e84181
--- /dev/null
+++ b/sources/Input/Input/Pointers.cs
@@ -0,0 +1,367 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Runtime.InteropServices;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Represents a collection of <see cref="IPointerDevice"/>s from which input events can be received.
+/// </summary>
+public sealed class Pointers
+    : InputContextDeviceList<IPointerDevice>,
+        IMouseInputHandler,
+        IPointerInputHandler
+{
+    private long _doubleClickTime;
+    private float _doubleClickRange;
+    private List<ClickData>? _clicks;
+
+    internal Pointers(InputContext ctx)
+        : base(ctx) => ClickConfiguration = PointerClickConfiguration.Default;
+
+    /// <summary>
+    /// Gets or sets the configuration that denotes the behaviour of <see cref="Click"/>/<see cref="DoubleClick"/>.
+    /// </summary>
+    public PointerClickConfiguration ClickConfiguration
+    {
+        get => new((int)((double)_doubleClickTime / Stopwatch.Frequency * 1000), _doubleClickRange);
+        set =>
+            (_doubleClickTime, _doubleClickRange) = (
+                (long)((double)value.DoubleClickTime / 1000 * Stopwatch.Frequency),
+                value.DoubleClickRange
+            );
+    }
+
+    /// <summary>
+    /// Raised when state pertaining to a pushable button on the pointer device changes (e.g. button up, button down).
+    /// </summary>
+    public event Action<ButtonChangedEvent<PointerButton>>? ButtonChanged;
+
+    /// <summary>
+    /// Raised when one or more <see cref="ButtonChanged"/> events indicate a single click as defined by the
+    /// <see cref="ClickConfiguration"/>.
+    /// </summary>
+    public event Action<PointerClickEvent>? Click;
+
+    /// <summary>
+    /// Raised when one or more <see cref="ButtonChanged"/> events indicate a double click as defined by the
+    /// <see cref="ClickConfiguration"/>.
+    /// </summary>
+    public event Action<PointerClickEvent>? DoubleClick;
+
+    /// <summary>
+    /// Raised when a <see cref="TargetPoint"/>'s state changes (e.g. mouse move).
+    /// </summary>
+    public event Action<PointChangedEvent>? PointChanged;
+
+    /// <summary>
+    /// Raised when a user scrolls using a pointer device's mouse wheel.
+    /// </summary>
+    public event Action<MouseScrollEvent>? MouseScroll;
+
+    /// <summary>
+    /// Raised when a "target" at which the user can point using a pointer device changes.
+    /// </summary>
+    public event Action<PointerTargetChangedEvent>? TargetChanged;
+
+    /// <summary>
+    /// Raised when the user adjusts their grip on the pointer device.
+    /// </summary>
+    public event Action<PointerGripChangedEvent>? GripChanged;
+
+    void IButtonInputHandler<PointerButton>.HandleButtonChanged(
+        ButtonChangedEvent<PointerButton> @event
+    ) => HandleButtonChanged(@event);
+
+    internal void HandleButtonChanged(ButtonChangedEvent<PointerButton> @event)
+    {
+        if (@event.Device is not IPointerDevice device)
+        {
+            return;
+        }
+
+        ButtonChanged?.Invoke(@event);
+        if (@event.Previous.IsDown || !@event.Button.IsDown)
+        {
+            return;
+        }
+
+        foreach (var target in device.Targets)
+        {
+            var pointCnt = target.GetPointCount(device);
+            for (var i = 0; i < pointCnt; i++)
+            {
+                HandlePointerDown(
+                    device,
+                    target.GetPoint(device, i),
+                    @event.Button.Name,
+                    @event.Timestamp
+                );
+            }
+        }
+    }
+
+    void IMouseInputHandler.HandleScroll(MouseScrollEvent @event) => HandleScroll(@event);
+
+    internal void HandleScroll(MouseScrollEvent @event) => MouseScroll?.Invoke(@event);
+
+    void IPointerInputHandler.HandleTargetChanged(PointerTargetChangedEvent @event) =>
+        HandleTargetChanged(@event);
+
+    internal void HandleTargetChanged(PointerTargetChangedEvent @event)
+    {
+        TargetChanged?.Invoke(@event);
+        if (_clicks is null || @event.IsAdded is not false)
+        {
+            return;
+        }
+
+        var clicks = CollectionsMarshal.AsSpan(_clicks);
+        for (var i = 0; i < clicks.Length; i++)
+        {
+            ref var click = ref clicks[i];
+            if (click.FirstClickPosition.Target != @event.Target)
+            {
+                continue;
+            }
+
+            // Raise a click event for posterity.
+            HandleDoubleClickExceedsParameters(ref click);
+            _clicks.RemoveAt(i--);
+
+            // SAFETY: We have to replace the span now as the RemoveAt could've in theory reallocated.
+            clicks = CollectionsMarshal.AsSpan(_clicks);
+        }
+    }
+
+    void IPointerInputHandler.HandlePointChanged(PointChangedEvent @event) =>
+        HandlePointChanged(@event);
+
+    internal void HandlePointChanged(PointChangedEvent @event)
+    {
+        PointChanged?.Invoke(@event);
+        if (_clicks is null || @event is not { OldPoint: not null, NewPoint: { } @new })
+        {
+            return;
+        }
+
+        var span = CollectionsMarshal.AsSpan(_clicks);
+        for (var i = 0; i < _clicks.Count; i++)
+        {
+            ref var click = ref span[i];
+            if (!click.IsMatch(@event.Pointer, in @new))
+            {
+                continue;
+            }
+
+            if (!click.HasMovedTooFar(_doubleClickRange, @new.Position))
+            {
+                return;
+            }
+
+            HandleDoubleClickExceedsParameters(ref click);
+            _clicks.RemoveAt(i);
+            return;
+        }
+    }
+
+    void IPointerInputHandler.HandleGripChanged(PointerGripChangedEvent @event) =>
+        HandleGripChanged(@event);
+
+    internal void HandleGripChanged(PointerGripChangedEvent @event) => GripChanged?.Invoke(@event);
+
+    private record struct ClickData(
+        IPointerDevice Device,
+        PointerButton? FirstClickButton,
+        TargetPoint FirstClickPosition,
+        long? FirstClickTime,
+        bool IsFirstClick
+    )
+    {
+        public bool IsMatch(IPointerDevice device, ref readonly TargetPoint point) =>
+            point.Id == FirstClickPosition.Id
+            && Device == device
+            && point.Target == FirstClickPosition.Target;
+
+        public bool HasMovedTooFar(float range, Vector3 position)
+        {
+            var fcp = FirstClickPosition.Position;
+            return MathF.Abs(position.X - fcp.X) >= range
+                && MathF.Abs(position.Y - fcp.Y) >= range
+                && MathF.Abs(position.Z - fcp.Z) >= range;
+        }
+    }
+
+    [MemberNotNull(nameof(_clicks))]
+    private ref ClickData GetClickData(
+        IPointerDevice device,
+        ref readonly TargetPoint point,
+        out int idx
+    )
+    {
+        idx = 0;
+        foreach (ref var ret in CollectionsMarshal.AsSpan(_clicks ??= []))
+        {
+            if (ret.IsMatch(device, in point))
+            {
+                return ref ret;
+            }
+
+            idx++;
+        }
+
+        _clicks.Add(
+            new ClickData(
+                device,
+                null,
+                default(TargetPoint) with
+                {
+                    Target = point.Target,
+                    Id = point.Id,
+                },
+                null,
+                true
+            )
+        );
+        return ref CollectionsMarshal.AsSpan(_clicks)[idx];
+    }
+
+    private void HandlePointerDown(
+        IPointerDevice device,
+        TargetPoint point,
+        PointerButton button,
+        long timestamp
+    )
+    {
+        if ((_clicks is null && DoubleClick is null && Click is null) || point.Target is null)
+        {
+            return;
+        }
+
+        ref var click = ref GetClickData(device, in point, out var idx);
+        if (click.IsFirstClick || (click.FirstClickButton is { } firstBtn && firstBtn != button))
+        {
+            // This is the first click with the given mouse button.
+            var time = click.FirstClickTime;
+            click.FirstClickTime = null;
+
+            if (
+                click is { IsFirstClick: false, FirstClickButton: { } prevBtn }
+                && time is { } clickTime
+            )
+            {
+                // Only the mouse buttons differ so treat last click as a single click.
+                Click?.Invoke(
+                    new PointerClickEvent(device, clickTime, click.FirstClickPosition, prevBtn)
+                );
+            }
+        }
+        else
+        {
+            // This is the second click with the same mouse button.
+            if (click.FirstClickTime is { } fct && timestamp - fct <= _doubleClickTime)
+            {
+                // Within the maximum double click time.
+                click.FirstClickTime = null;
+                if (!click.HasMovedTooFar(_doubleClickRange, point.Position))
+                {
+                    // Second click was in time and in range -> double click.
+                    DoubleClick?.Invoke(new PointerClickEvent(device, timestamp, point, button));
+
+                    // SAFETY: Must not use the click ref from now on! Returning instantly.
+                    _clicks.RemoveAt(idx);
+                    return;
+                }
+
+                // Second click was in time but outside range -> single click.
+                // The second click is another "first click".
+                Click?.Invoke(new PointerClickEvent(device, timestamp, point, button));
+            }
+            else
+            {
+                // The double click time elapsed.
+
+                // If Update() would have detected the time elapse before,
+                // it would have set _firstClick back to true and we won't be here.
+                // Therefore Update() has not detected time elapse here and we have
+                // to handle it.
+                HandleDoubleClickExceedsParameters(ref click);
+            }
+        }
+
+        // Process the first click. We process the second click as another "first click" if:
+        // - the double click time elapsed
+        // - the pointer moved too much before doing the second click
+        ProcessFirstClick(ref click, button, point, timestamp);
+    }
+
+    private static void ProcessFirstClick(
+        ref ClickData click,
+        PointerButton button,
+        TargetPoint point,
+        long timestamp
+    )
+    {
+        click.IsFirstClick = false; // for next time...
+        click.FirstClickButton = button;
+        click.FirstClickPosition = point;
+        click.FirstClickTime = timestamp;
+    }
+
+    private void HandleDoubleClickExceedsParameters(ref ClickData click)
+    {
+        click.FirstClickTime = null;
+        click.IsFirstClick = true;
+        if (click is { FirstClickButton: { } fcb, FirstClickTime: { } fct })
+        {
+            Click?.Invoke(new PointerClickEvent(click.Device, fct, click.FirstClickPosition, fcb));
+        }
+    }
+
+    internal void HandleUpdate()
+    {
+        if (_clicks is null)
+        {
+            return;
+        }
+
+        var updateTime = Stopwatch.GetTimestamp();
+        var clicks = CollectionsMarshal.AsSpan(_clicks);
+        for (var i = 0; i < clicks.Length; i++)
+        {
+            ref var click = ref clicks[i];
+            if (click.FirstClickTime is not { } fct || updateTime - fct <= _doubleClickTime)
+            {
+                continue;
+            }
+
+            // No second click in maximum double click time.
+            HandleDoubleClickExceedsParameters(ref click);
+            _clicks.RemoveAt(i--);
+
+            // SAFETY: We have to replace the span now as the RemoveAt could've in theory reallocated.
+            clicks = CollectionsMarshal.AsSpan(_clicks);
+        }
+    }
+
+    /// <inheritdoc />
+    protected internal override void HandleDeviceConnectionChanged(ConnectionEvent @event)
+    {
+        base.HandleDeviceConnectionChanged(@event);
+        if (_clicks is null || @event.IsConnected || @event.Device is not IPointerDevice)
+        {
+            return;
+        }
+
+        for (var i = 0; i < _clicks.Count; i++)
+        {
+            if (_clicks[i].Device != @event.Device)
+            {
+                continue;
+            }
+
+            _clicks.RemoveAt(i--);
+        }
+    }
+}
diff --git a/sources/Input/Input/Silk.NET.Input.csproj b/sources/Input/Input/Silk.NET.Input.csproj
new file mode 100644
index 0000000000..d20067094e
--- /dev/null
+++ b/sources/Input/Input/Silk.NET.Input.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+        <NoWarn>ST0005;$(NoWarn)</NoWarn>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\..\Core\Core\Silk.NET.Core.csproj" />
+      <ProjectReference Include="..\..\Maths\Maths\Silk.NET.Maths.csproj" />
+      <ProjectReference Include="..\..\SDL\SDL\Silk.NET.SDL.csproj" />
+    </ItemGroup>
+  <Import Project="..\..\Core\Core\Silk.NET.Core.targets" />
+
+</Project>
diff --git a/sources/Input/Input/Silk.NET.Input.csproj.DotSettings b/sources/Input/Input/Silk.NET.Input.csproj.DotSettings
new file mode 100644
index 0000000000..10361e88b3
--- /dev/null
+++ b/sources/Input/Input/Silk.NET.Input.csproj.DotSettings
@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=implementations/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file
diff --git a/sources/Input/Input/TargetPoint.cs b/sources/Input/Input/TargetPoint.cs
new file mode 100644
index 0000000000..4607643ae7
--- /dev/null
+++ b/sources/Input/Input/TargetPoint.cs
@@ -0,0 +1,54 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using Silk.NET.Maths;
+
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Represents a point on a target at which a pointer is pointing.
+/// </summary>
+/// <param name="Id">
+/// An integral identifier for the point. This point must be the only point for the device currently pointing at a
+/// target with this identifier at any given time. If this point ceases to point at the target, then the identifier
+/// becomes free for another device point. This means that this identifier can just be a counter, but may be globally
+/// unique depending on the backend's capabilities. If an index is used, points with greater indices should not be
+/// "moved" into this point's place should it no longer point at the target. This is to allow applications to track
+/// distinct points.
+/// </param>
+/// <param name="Flags">Flags describing the state of the point.</param>
+/// <param name="Position">The absolute position on the target at which the pointer is pointing.</param>
+/// <param name="NormalizedPosition">
+/// The normalized position on the target at which the pointer is pointing, if applicable. If this is not available
+/// (e.g. due to the target being infinitely large a.k.a. "unbounded"), then this property shall have a value of
+/// <c>default</c>.
+/// </param>
+/// <param name="Pointer">
+/// A ray representing the distance and angle at which the pointer is pointing at the point on the target. A ray with an
+/// orientation equivalent to an identity quaternion shall be interpreted as the point directly perpendicular to and
+/// facing towards the target, with this being the default value should this information be unavailable. If distance
+/// information is unavailable, this shall be equivalent to a <c>default</c> vector.
+/// </param>
+/// <param name="Pressure">
+/// The pressure applied to the point on the target by the pointer, between <c>0.0</c> representing the minimum amount
+/// of pressure and <c>1.0</c> representing the maximum amount of pressure. This shall be <c>1.0</c> if such data is
+/// unavailable but the point is otherwise valid.
+/// </param>
+/// <param name="Target">The pointer being pointed at.</param>
+public readonly record struct TargetPoint(
+    int Id,
+    TargetPointFlags Flags,
+    Vector3 Position,
+    Vector3 NormalizedPosition,
+    Ray3D<float> Pointer,
+    float Pressure,
+    IPointerTarget? Target
+)
+{
+    /// <summary>
+    /// Gets a value indicating whether this <see cref="TargetPoint"/> is a valid instance of a point on a
+    /// <see cref="Target"/> that the user is pointing at using their pointer device.
+    /// </summary>
+    [MemberNotNullWhen(true, nameof(Target))]
+    public bool IsValid =>
+        (Flags & TargetPointFlags.PointingAtTarget) != TargetPointFlags.NotPointingAtTarget;
+}
diff --git a/sources/Input/Input/TargetPointFlags.cs b/sources/Input/Input/TargetPointFlags.cs
new file mode 100644
index 0000000000..091cb74fb4
--- /dev/null
+++ b/sources/Input/Input/TargetPointFlags.cs
@@ -0,0 +1,18 @@
+namespace Silk.NET.Input;
+
+/// <summary>
+/// Flags describing a <see cref="TargetPoint" /> state.
+/// </summary>
+[Flags]
+public enum TargetPointFlags
+{
+    /// <summary>
+    /// No flags are set, indicating that the point is not being pointed at and therefore may not be valid.
+    /// </summary>
+    NotPointingAtTarget = 0,
+
+    /// <summary>
+    /// Indicates that the point has been resolved as a valid point at which the pointer is pointing.
+    /// </summary>
+    PointingAtTarget = 1 << 0
+}
\ No newline at end of file
diff --git a/tests/Input/Input/InputMarshalTests.cs b/tests/Input/Input/InputMarshalTests.cs
new file mode 100644
index 0000000000..e75c730077
--- /dev/null
+++ b/tests/Input/Input/InputMarshalTests.cs
@@ -0,0 +1,160 @@
+// 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.Reflection;
+using NUnit.Framework;
+
+namespace Silk.NET.Input.UnitTests;
+
+[TestFixture]
+public class InputMarshalTests
+{
+    [TestCase(TypeArgs = [typeof(PointerButton)])]
+    [TestCase(TypeArgs = [typeof(JoystickButton)])]
+    [TestCase(TypeArgs = [typeof(KeyName)])]
+    public void GetButtonCount<T>()
+        where T : unmanaged, Enum
+    {
+        // This is to determine the gaps in KeyName.
+        var prev = -1;
+        foreach (var @enum in Enum.GetValues<T>().Order())
+        {
+            var val = SilkMarshal.ConstCast<T, int>(@enum);
+            if (val - 1 != prev)
+            {
+                Console.WriteLine(
+                    $"{prev} ({SilkMarshal.ConstCast<int, T>(prev)}), {val} ({@enum})"
+                );
+            }
+
+            prev = val;
+        }
+    }
+
+    [TestCase(TypeArgs = [typeof(PointerButton)])]
+    [TestCase(TypeArgs = [typeof(JoystickButton)])]
+    [TestCase(TypeArgs = [typeof(KeyName)])]
+    public void EnumerateButtonList<T>()
+        where T : unmanaged, Enum
+    {
+        var list = InputMarshal.CreateList<Button<T>>();
+        var expectedCount = Enum.GetNames(typeof(T))
+            .DistinctBy(Enum.Parse<T>)
+            .Count(x => x != "Unknown");
+        Assert.That(list.List, Has.Count.EqualTo(expectedCount));
+        var encountered = 0;
+        var values = Enum.GetValues<T>()
+            .Where(x => x.ToString() != "Unknown")
+            .Distinct()
+            .Order()
+            .GetEnumerator();
+        foreach (var btn in list.List)
+        {
+            encountered++;
+            Assert.Multiple(() =>
+            {
+                Assert.That(values.MoveNext(), Is.True);
+                Assert.That(btn.Name, Is.EqualTo(values.Current));
+                Assert.That(btn.Pressure, Is.EqualTo(0));
+                Assert.That(btn.IsDown, Is.False);
+            });
+        }
+
+        Assert.That(encountered, Is.EqualTo(expectedCount));
+    }
+
+    [TestCase(TypeArgs = [typeof(PointerButton)])]
+    [TestCase(TypeArgs = [typeof(JoystickButton)])]
+    [TestCase(TypeArgs = [typeof(KeyName)])]
+    public void IndexButtonList<T>()
+        where T : unmanaged, Enum
+    {
+        var list = InputMarshal.CreateList<Button<T>>();
+        var idx = 0;
+        foreach (
+            var name in Enum.GetValues<T>().Where(x => x.ToString() != "Unknown").Distinct().Order()
+        )
+        {
+            var btn = list.List[idx++];
+            Assert.Multiple(() =>
+            {
+                Assert.That(btn.Name, Is.EqualTo(name));
+                Assert.That(btn.Pressure, Is.EqualTo(0));
+                Assert.That(btn.IsDown, Is.False);
+            });
+        }
+    }
+
+    [TestCase(TypeArgs = [typeof(PointerButton)])]
+    [TestCase(TypeArgs = [typeof(JoystickButton)])]
+    [TestCase(TypeArgs = [typeof(KeyName)])]
+    public void GetButtonState<T>()
+        where T : unmanaged, Enum
+    {
+        var list = InputMarshal.CreateList<Button<T>>().List.AsButtonList();
+        foreach (
+            var name in Enum.GetValues<T>().Where(x => x.ToString() != "Unknown").Distinct().Order()
+        )
+        {
+            var btn = list[name];
+            Assert.Multiple(() =>
+            {
+                Assert.That(btn.Name, Is.EqualTo(name));
+                Assert.That(btn.Pressure, Is.EqualTo(0));
+                Assert.That(btn.IsDown, Is.False);
+            });
+        }
+    }
+
+    [TestCase(TypeArgs = [typeof(PointerButton)])]
+    [TestCase(TypeArgs = [typeof(JoystickButton)])]
+    [TestCase(TypeArgs = [typeof(KeyName)])]
+    public void IndexNameTranslationRoundTrip<T>()
+        where T : unmanaged, Enum
+    {
+        var values = Enum.GetValues<T>()
+            .Where(x => x.ToString() != "Unknown")
+            .Distinct()
+            .Order()
+            .GetEnumerator();
+        for (var i = 0; i < InputMarshal.GetButtonListCount<T>(); i++)
+        {
+            Assert.That(values.MoveNext(), Is.True);
+            var name = InputMarshal.ButtonList<T>.IndexName(i);
+            Assert.Multiple(() =>
+            {
+                Assert.That(name, Is.EqualTo(values.Current));
+                Assert.That(InputMarshal.ButtonList<T>.NameIndex(name), Is.EqualTo(i));
+            });
+        }
+    }
+
+    [TestCase(TypeArgs = [typeof(PointerButton)])]
+    [TestCase(TypeArgs = [typeof(JoystickButton)])]
+    [TestCase(TypeArgs = [typeof(KeyName)])]
+    public void SetGetBinaryButtonState<T>()
+        where T : unmanaged, Enum
+    {
+        var arr = Enum.GetValues<T>()
+            .Where(x => x.ToString() != "Unknown")
+            .Distinct()
+            .Order()
+            .ToArray();
+        foreach (var name in arr)
+        {
+            var list = InputMarshal.CreateList<Button<T>>();
+            InputMarshal.SetButtonState(list, new Button<T>(name, true, 1), true);
+            foreach (var testName in arr)
+            {
+                var btn = list.List.AsButtonList()[testName];
+                Assert.Multiple(() =>
+                {
+                    Assert.That(btn.Name, Is.EqualTo(testName));
+                    Assert.That(btn.Pressure, Is.EqualTo(testName.Equals(name) ? 1 : 0));
+                    Assert.That(btn.IsDown, Is.EqualTo(testName.Equals(name)));
+                });
+            }
+        }
+    }
+}
diff --git a/tests/Input/Input/Silk.NET.Input.UnitTests.csproj b/tests/Input/Input/Silk.NET.Input.UnitTests.csproj
new file mode 100644
index 0000000000..2daa2e92e6
--- /dev/null
+++ b/tests/Input/Input/Silk.NET.Input.UnitTests.csproj
@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\..\sources\Input\Input\Silk.NET.Input.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\..\..\sources\Core\Core\Silk.NET.Core.cs">
+      <Link>Silk.NET.Core.cs</Link>
+    </Compile>
+  </ItemGroup>
+
+</Project>