aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Gamepad/SdlGamepadSource.cs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
committergodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
commit39066900773e7857faf2d02e7ed51b71d219d97d (patch)
treeffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Gamepad/SdlGamepadSource.cs
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Gamepad/SdlGamepadSource.cs')
-rw-r--r--src/Godosa.Gamepad/SdlGamepadSource.cs107
1 files changed, 107 insertions, 0 deletions
diff --git a/src/Godosa.Gamepad/SdlGamepadSource.cs b/src/Godosa.Gamepad/SdlGamepadSource.cs
new file mode 100644
index 0000000..ee63161
--- /dev/null
+++ b/src/Godosa.Gamepad/SdlGamepadSource.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Numerics;
+using Godosa.Core.Diagnostics;
+using Godosa.Core.Input;
+using Silk.NET.SDL;
+
+namespace Godosa.Gamepad;
+
+/// <summary>
+/// Reads the first attached game controller through windowless SDL. Returns raw
+/// held state; each game maps it (and applies deadzones). Any SDL failure disables
+/// the pad for the session and the game stays keyboard-only.
+/// </summary>
+public sealed unsafe class SdlGamepadSource : IDisposable
+{
+ private Sdl? _sdl;
+ private GameController* _pad;
+ private bool _failed;
+
+ public GamepadState? Poll()
+ {
+ if (_failed) return null;
+ try
+ {
+ if (_sdl is null && !Init()) return null;
+ var sdl = _sdl!;
+ sdl.GameControllerUpdate(); // also detects hot-plug (~1.4 µs/call)
+
+ if (_pad != null && sdl.GameControllerGetAttached(_pad) != SdlBool.True)
+ {
+ sdl.GameControllerClose(_pad);
+ _pad = null;
+ if (EngineDebug.Input) EngineDebug.Write("input", "gamepad detached");
+ }
+ if (_pad == null && !Open(sdl)) return null;
+
+ var buttons = PadButtons.None;
+ foreach (var (sdlButton, pad) in ButtonMap)
+ if (sdl.GameControllerGetButton(_pad, sdlButton) != 0) buttons |= pad;
+ return new GamepadState(
+ new Vector2(Axis(sdl, GameControllerAxis.Leftx), Axis(sdl, GameControllerAxis.Lefty)),
+ new Vector2(Axis(sdl, GameControllerAxis.Rightx), Axis(sdl, GameControllerAxis.Righty)),
+ Math.Max(0f, Axis(sdl, GameControllerAxis.Triggerleft)),
+ Math.Max(0f, Axis(sdl, GameControllerAxis.Triggerright)),
+ buttons);
+ }
+ catch (Exception ex)
+ {
+ if (EngineDebug.Input) EngineDebug.Write("input", $"gamepad disabled: {ex.Message}");
+ _failed = true;
+ return null;
+ }
+ }
+
+ private static readonly (GameControllerButton Sdl, PadButtons Pad)[] ButtonMap =
+ {
+ (GameControllerButton.A, PadButtons.A), (GameControllerButton.B, PadButtons.B),
+ (GameControllerButton.X, PadButtons.X), (GameControllerButton.Y, PadButtons.Y),
+ (GameControllerButton.Back, PadButtons.Back), (GameControllerButton.Start, PadButtons.Start),
+ (GameControllerButton.Leftshoulder, PadButtons.LeftShoulder), (GameControllerButton.Rightshoulder, PadButtons.RightShoulder),
+ (GameControllerButton.DpadUp, PadButtons.DpadUp), (GameControllerButton.DpadDown, PadButtons.DpadDown),
+ (GameControllerButton.DpadLeft, PadButtons.DpadLeft), (GameControllerButton.DpadRight, PadButtons.DpadRight),
+ };
+
+ private bool Init()
+ {
+ var sdl = Sdl.GetApi();
+ sdl.SetHint(Sdl.HintJoystickAllowBackgroundEvents, "1"); // the Avalonia window has focus, not SDL
+ sdl.SetHint(Sdl.HintNoSignalHandlers, "1");
+ if (sdl.InitSubSystem(Sdl.InitGamecontroller) != 0)
+ {
+ if (EngineDebug.Input) EngineDebug.Write("input", $"gamepad disabled: {sdl.GetErrorS()}");
+ _failed = true;
+ return false;
+ }
+ sdl.JoystickEventState(0); // SDL_IGNORE: state is polled; nothing drains SDL's event queue
+ sdl.GameControllerEventState(0);
+ _sdl = sdl;
+ return true;
+ }
+
+ private bool Open(Sdl sdl)
+ {
+ int count = sdl.NumJoysticks();
+ for (int i = 0; i < count; i++)
+ {
+ if (sdl.IsGameController(i) != SdlBool.True) continue;
+ _pad = sdl.GameControllerOpen(i);
+ if (_pad == null) continue;
+ if (EngineDebug.Input) EngineDebug.Write("input", $"gamepad attached: {sdl.GameControllerNameS(_pad)}");
+ return true;
+ }
+ return false;
+ }
+
+ private float Axis(Sdl sdl, GameControllerAxis axis) =>
+ Math.Clamp(sdl.GameControllerGetAxis(_pad, axis) / 32767f, -1f, 1f);
+
+ public void Dispose()
+ {
+ if (_sdl is null) return;
+ if (_pad != null) _sdl.GameControllerClose(_pad);
+ _pad = null;
+ _sdl.QuitSubSystem(Sdl.InitGamecontroller);
+ _sdl = null;
+ }
+}