From 397bdda076f8e5668a354b836d65be5827bb02e3 Mon Sep 17 00:00:00 2001 From: thefiddler Date: Thu, 7 Aug 2014 18:08:53 +0200 Subject: [PATCH 1/5] [Linux] Implemented evdev joystick device discovery --- .../OpenTK/Platform/Linux/Bindings/Evdev.cs | 157 ++++++- Source/OpenTK/Platform/Linux/Bindings/Libc.cs | 18 +- Source/OpenTK/Platform/Linux/LinuxJoystick.cs | 401 ++++++++++-------- 3 files changed, 386 insertions(+), 190 deletions(-) diff --git a/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs b/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs index 11988ebe..e37cce79 100644 --- a/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs +++ b/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs @@ -29,6 +29,7 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; using OpenTK.Input; namespace OpenTK.Platform.Linux @@ -36,6 +37,10 @@ namespace OpenTK.Platform.Linux // Bindings for linux/input.h class Evdev { + public const int KeyCount = 0x300; + public const int AxisCount = 0x40; + public const int EventCount = (int)EvdevType.CNT; + #region KeyMap public static readonly Key[] KeyMap = new Key[] @@ -365,9 +370,101 @@ namespace OpenTK.Platform.Linux return MouseButton.Left; } } + + static uint IOCreate(DirectionFlags dir, int number, int length) + { + long v = + ((byte)dir << 30) | + ((byte)'E' << 8) | + (number << 0) | + (length << 16); + return (uint)v; + } + + public static int GetBit(int fd, EvdevType ev, int length, IntPtr data) + { + // EVIOCGBIT = _IOC(_IOC_READ, 'E', 0x20 + (ev), len) + uint ioctl = IOCreate(DirectionFlags.Read, (int)ev + 0x20, length); + int retval = Libc.ioctl(fd, ioctl, data); + return retval; + } + + public static int GetName(int fd, out string name) + { + unsafe + { + sbyte* pname = stackalloc sbyte[129]; + int ret = Libc.ioctl(fd, EvdevIoctl.Name128, new IntPtr(pname)); + name = new string(pname); + return ret; + } + } + + public static int GetId(int fd, out EvdevInputId id) + { + id = default(EvdevInputId); + unsafe + { + fixed (EvdevInputId* pid = &id) + { + return Libc.ioctl(fd, EvdevIoctl.Id, new IntPtr(pid)); + } + } + } } - enum EvdevButton : uint + enum EvdevAxis + { + X = 0x00, + Y = 0x01, + Z = 0x02, + RX = 0x03, + RY = 0x04, + RZ = 0x05, + THROTTLE = 0x06, + RUDDER = 0x07, + WHEEL = 0x08, + GAS = 0x09, + BRAKE = 0x0a, + HAT0X = 0x10, + HAT0Y = 0x11, + HAT1X = 0x12, + HAT1Y = 0x13, + HAT2X = 0x14, + HAT2Y = 0x15, + HAT3X = 0x16, + HAT3Y = 0x17, + PRESSURE = 0x18, + DISTANCE = 0x19, + TILT_X = 0x1a, + TILT_Y = 0x1b, + TOOL_WIDTH = 0x1c, + + VOLUME = 0x20, + + MISC = 0x28, + + MT_SLOT = 0x2f, /* MT slot being modified */ + MT_TOUCH_MAJOR = 0x30, /* Major axis of touching ellipse */ + MT_TOUCH_MINOR = 0x31, /* Minor axis (omit if circular) */ + MT_WIDTH_MAJOR = 0x32, /* Major axis of approaching ellipse */ + MT_WIDTH_MINOR = 0x33, /* Minor axis (omit if circular) */ + MT_ORIENTATION = 0x34, /* Ellipse orientation */ + MT_POSITION_X = 0x35, /* Center X touch position */ + MT_POSITION_Y = 0x36, /* Center Y touch position */ + MT_TOOL_TYPE = 0x37, /* Type of touching device */ + MT_BLOB_ID = 0x38, /* Group a set of packets as a blob */ + MT_TRACKING_ID = 0x39, /* Unique ID of initiated contact */ + MT_PRESSURE = 0x3a, /* Pressure on contact area */ + MT_DISTANCE = 0x3b, /* Contact hover distance */ + MT_TOOL_X = 0x3c, /* Center X tool position */ + MT_TOOL_Y = 0x3d, /* Center Y tool position */ + + MAX = 0x3f, + CNT = (MAX+1), + } + + enum EvdevButton { MISC = 0x100, BTN0 = 0x100, @@ -447,6 +544,64 @@ namespace OpenTK.Platform.Linux WHEEL = 0x150, GEAR_DOWN = 0x150, GEAR_UP = 0x151, + + DPAD_UP = 0x220, + DPAD_DOWN = 0x221, + DPAD_LEFT = 0x222, + DPAD_RIGHT = 0x223, + + Last = 0x300, + } + + enum EvdevType : byte + { + SYN = 0x00, + KEY = 0x01, + REL = 0x02, + ABS = 0x03, + MSC = 0x04, + SW = 0x05, + LED = 0x11, + SND = 0x12, + REP = 0x14, + FF = 0x15, + PWR = 0x16, + FF_STATUS = 0x17, + MAX = 0x1f, + CNT = (MAX+1), + } + + enum EvdevIoctl : uint + { + Id = (2u << 30) | ((byte)'E' << 8) | (0x02u << 0) | (8u << 16), //EVIOCGID = _IOR('E', 0x02, struct input_id) + Name128 = (2u << 30) | ((byte)'E' << 8) | (0x06u << 0) | (128u << 16), //EVIOCGNAME(len) = _IOC(_IOC_READ, 'E', 0x06, len) + } + + [StructLayout(LayoutKind.Sequential)] + struct InputId + { + public ushort BusType; + public ushort Vendor; + public ushort Product; + public ushort Version; + } + + [StructLayout(LayoutKind.Sequential)] + struct InputEvent + { + public TimeVal Time; + ushort type; + public ushort Code; + public int Value; + + public EvdevType Type { get { return (EvdevType)type; } } + } + + [StructLayout(LayoutKind.Sequential)] + struct TimeVal + { + public IntPtr Seconds; + public IntPtr MicroSeconds; } } diff --git a/Source/OpenTK/Platform/Linux/Bindings/Libc.cs b/Source/OpenTK/Platform/Linux/Bindings/Libc.cs index 4b29624f..11318756 100644 --- a/Source/OpenTK/Platform/Linux/Bindings/Libc.cs +++ b/Source/OpenTK/Platform/Linux/Bindings/Libc.cs @@ -52,7 +52,10 @@ namespace OpenTK.Platform.Linux public static extern int ioctl(int d, JoystickIoctlCode request, StringBuilder data); [DllImport(lib)] - public static extern int ioctl(int d, EvdevIoctlCode request, out EvdevInputId data); + public static extern int ioctl(int d, EvdevIoctl request, [Out] IntPtr data); + + [DllImport(lib)] + public static extern int ioctl(int d, uint request, [Out] IntPtr data); [DllImport(lib)] public static extern int ioctl(int d, KeyboardIoctlCode request, ref IntPtr data); @@ -106,6 +109,14 @@ namespace OpenTK.Platform.Linux InvalidValue = 22, } + [Flags] + enum DirectionFlags + { + None = 0, + Write = 1, + Read = 2 + } + [Flags] enum OpenFlags { @@ -116,11 +127,6 @@ namespace OpenTK.Platform.Linux CloseOnExec = 0x0080000 } - enum EvdevIoctlCode : uint - { - Id = ((byte)'E' << 8) | (0x02 << 0) //EVIOCGID, which is _IOR('E', 0x02, struct input_id) - } - [Flags] enum JoystickEventType : byte { diff --git a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs index 51992797..39a2f884 100644 --- a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs +++ b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs @@ -35,14 +35,23 @@ using OpenTK.Input; namespace OpenTK.Platform.Linux { - struct LinuxJoyDetails + class LinuxJoystickDetails { public Guid Guid; + public string Name; public int FileDescriptor; + public int PathIndex; // e.g. "0" for "/dev/input/event0". Used as a hardware id public JoystickState State; + public JoystickCapabilities Caps; + + public readonly Dictionary AxisMap = + new Dictionary(); + public readonly Dictionary ButtonMap = + new Dictionary(); + public readonly Dictionary HatMap = + new Dictionary(); } - // Note: despite what the name says, this class is Linux-specific. sealed class LinuxJoystick : IJoystickDriver2 { #region Fields @@ -51,8 +60,8 @@ namespace OpenTK.Platform.Linux readonly FileSystemWatcher watcher = new FileSystemWatcher(); - readonly Dictionary index_to_stick = new Dictionary(); - List> sticks = new List>(); + readonly DeviceCollection Sticks = + new DeviceCollection(); bool disposed; @@ -89,12 +98,10 @@ namespace OpenTK.Platform.Linux { foreach (string file in Directory.GetFiles(path)) { - JoystickDevice stick = OpenJoystick(file); + LinuxJoystickDetails stick = OpenJoystick(file); if (stick != null) { - //stick.Description = String.Format("USB Joystick {0} ({1} axes, {2} buttons, {3}{0})", - //number, stick.Axis.Count, stick.Button.Count, path); - sticks.Add(stick); + Sticks.Add(stick.PathIndex, stick); } } } @@ -102,10 +109,11 @@ namespace OpenTK.Platform.Linux int GetJoystickNumber(string path) { - if (path.StartsWith("js")) + const string evdev = "event"; + if (path.StartsWith(evdev)) { int num; - if (Int32.TryParse(path.Substring(2), out num)) + if (Int32.TryParse(path.Substring(evdev.Length), out num)) { return num; } @@ -129,23 +137,11 @@ namespace OpenTK.Platform.Linux int number = GetJoystickNumber(file); if (number != -1) { - // Find which joystick id matches this number - int i; - for (i = 0; i < sticks.Count; i++) + var stick = Sticks.FromHardwareId(number); + if (stick != null) { - if (sticks[i].Id == number) - { - break; - } - } - - if (i == sticks.Count) - { - Debug.Print("[Evdev] Joystick id {0} does not exist.", number); - } - else - { - CloseJoystick(sticks[i]); + CloseJoystick(stick); + Sticks.TryRemove(number); } } } @@ -155,75 +151,129 @@ namespace OpenTK.Platform.Linux #region Private Members - Guid CreateGuid(JoystickDevice js, string path, int number) + Guid CreateGuid(EvdevInputId id, string name) { byte[] bytes = new byte[16]; - for (int i = 0; i < Math.Min(bytes.Length, js.Description.Length); i++) + + int i = 0; + byte[] bus = BitConverter.GetBytes(id.BusType); + bytes[i++] = bus[0]; + bytes[i++] = bus[1]; + bytes[i++] = 0; + bytes[i++] = 0; + + if (id.Vendor != 0 && id.Product != 0 && id.Version != 0) { - bytes[i] = (byte)js.Description[i]; + byte[] vendor = BitConverter.GetBytes(id.Vendor); + byte[] product = BitConverter.GetBytes(id.Product); + byte[] version = BitConverter.GetBytes(id.Version); + bytes[i++] = vendor[0]; + bytes[i++] = vendor[1]; + bytes[i++] = 0; + bytes[i++] = 0; + bytes[i++] = product[0]; + bytes[i++] = product[1]; + bytes[i++] = 0; + bytes[i++] = 0; + bytes[i++] = version[0]; + bytes[i++] = version[1]; + bytes[i++] = 0; + bytes[i++] = 0; } - return new Guid(bytes); - -#if false // Todo: move to /dev/input/event* from /dev/input/js* - string evdev_path = Path.Combine(Path.GetDirectoryName(path), "event" + number); - if (!File.Exists(evdev_path)) - return new Guid(); - - int event_fd = UnsafeNativeMethods.open(evdev_path, OpenFlags.NonBlock); - if (event_fd < 0) - return new Guid(); - - try + else { - EventInputId id; - if (UnsafeNativeMethods.ioctl(event_fd, EvdevInputId.Id, out id) < 0) - return new Guid(); - - int i = 0; - byte[] bus = BitConverter.GetBytes(id.BusType); - bytes[i++] = bus[0]; - bytes[i++] = bus[1]; - bytes[i++] = 0; - bytes[i++] = 0; - - if (id.Vendor != 0 && id.Product != 0 && id.Version != 0) + for (int j = 0; j < bytes.Length - i; j++) { - byte[] vendor = BitConverter.GetBytes(id.Vendor); - byte[] product = BitConverter.GetBytes(id.Product); - byte[] version = BitConverter.GetBytes(id.Version); - bytes[i++] = vendor[0]; - bytes[i++] = vendor[1]; - bytes[i++] = 0; - bytes[i++] = 0; - bytes[i++] = product[0]; - bytes[i++] = product[1]; - bytes[i++] = 0; - bytes[i++] = 0; - bytes[i++] = version[0]; - bytes[i++] = version[1]; - bytes[i++] = 0; - bytes[i++] = 0; + bytes[i + j] = (byte)name[j]; + } + } + + return new Guid(bytes); + } + + unsafe static bool TestBit(byte* ptr, int bit) + { + int byte_offset = bit / 8; + int bit_offset = bit % 8; + return (*(ptr + byte_offset) & (1 << bit_offset)) != 0; + } + + unsafe static int AddAxes(LinuxJoystickDetails stick, byte* axisbit, int bytecount) + { + JoystickAxis axes = 0; + JoystickHat hats = 0; + int bitcount = bytecount * 8; + for (EvdevAxis axis = 0; axis < EvdevAxis.CNT && (int)axis < bitcount; axis++) + { + if (axis >= EvdevAxis.HAT0X && axis <= EvdevAxis.HAT3Y) + { + // Axis is analogue hat - skip + continue; + } + + if (TestBit(axisbit, (int)axis)) + { + stick.AxisMap.Add(axis, axes++); } else { - for (; i < bytes.Length; i++) - { - bytes[i] = (byte)js.Description[i]; - } + stick.AxisMap.Add(axis, (JoystickAxis)(-1)); + } + } + return (int)axes; + } + + unsafe static int AddButtons(LinuxJoystickDetails stick, byte* keybit, int bytecount) + { + JoystickButton buttons = 0; + int bitcount = bytecount * 8; + for (EvdevButton button = 0; button < EvdevButton.Last && (int)button < bitcount; button++) + { + if (button >= EvdevButton.DPAD_UP && button <= EvdevButton.DPAD_RIGHT) + { + // Button is dpad (hat) - skip + continue; } - return new Guid(bytes); + if (TestBit(keybit, (int)button)) + { + stick.ButtonMap.Add(button, buttons++); + } + else + { + stick.ButtonMap.Add(button, (JoystickButton)(-1)); + } } - finally - { - UnsafeNativeMethods.close(event_fd); - } -#endif + return (int)buttons; } - - JoystickDevice OpenJoystick(string path) + + unsafe static int AddHats(LinuxJoystickDetails stick, + byte* axisbit, int axiscount, + byte* keybit, int keycount) { - JoystickDevice stick = null; + JoystickHat hats = 0; + for (EvdevAxis hat = EvdevAxis.HAT0X; hat < EvdevAxis.HAT3Y && (int)hat < axiscount * 8; hat++) + { + if (TestBit(axisbit, (int)hat)) + { + stick.HatMap.Add((int)hat, hats++); + } + } + + for (EvdevButton dpad = EvdevButton.DPAD_UP; dpad < EvdevButton.DPAD_RIGHT && (int)dpad < keycount * 8; dpad++) + { + if (TestBit(axisbit, (int)dpad)) + { + stick.HatMap.Add((int)dpad, hats++); + } + } + + return (int)hats; + } + + LinuxJoystickDetails OpenJoystick(string path) + { + LinuxJoystickDetails stick = null; int number = GetJoystickNumber(Path.GetFileName(path)); if (number >= 0) @@ -235,123 +285,114 @@ namespace OpenTK.Platform.Linux if (fd == -1) return null; - // Check joystick driver version (must be 1.0+) - int driver_version = 0x00000800; - Libc.ioctl(fd, JoystickIoctlCode.Version, ref driver_version); - if (driver_version < 0x00010000) - return null; - - // Get number of joystick axes - int axes = 0; - Libc.ioctl(fd, JoystickIoctlCode.Axes, ref axes); - - // Get number of joystick buttons - int buttons = 0; - Libc.ioctl(fd, JoystickIoctlCode.Buttons, ref buttons); - - stick = new JoystickDevice(number, axes, buttons); - - StringBuilder sb = new StringBuilder(128); - Libc.ioctl(fd, JoystickIoctlCode.Name128, sb); - stick.Description = sb.ToString(); - - stick.Details.FileDescriptor = fd; - stick.Details.State.SetIsConnected(true); - stick.Details.Guid = CreateGuid(stick, path, number); - - // Find the first disconnected joystick (if any) - int i; - for (i = 0; i < sticks.Count; i++) + unsafe { - if (!sticks[i].Details.State.IsConnected) + const int evsize = Evdev.EventCount / 8; + const int axissize = Evdev.AxisCount / 8; + const int keysize = Evdev.KeyCount / 8; + byte* evbit = stackalloc byte[evsize]; + byte* axisbit = stackalloc byte[axissize]; + byte* keybit = stackalloc byte[keysize]; + + string name; + EvdevInputId id; + + // Ensure this is a joystick device + bool is_valid = true; + + is_valid &= Evdev.GetBit(fd, 0, evsize, new IntPtr(evbit)) >= 0; + is_valid &= Evdev.GetBit(fd, EvdevType.ABS, axissize, new IntPtr(axisbit)) >= 0; + is_valid &= Evdev.GetBit(fd, EvdevType.KEY, keysize, new IntPtr(keybit)) >= 0; + + is_valid &= TestBit(evbit, (int)EvdevType.KEY); + is_valid &= TestBit(evbit, (int)EvdevType.ABS); + is_valid &= TestBit(axisbit, (int)EvdevAxis.X); + is_valid &= TestBit(axisbit, (int)EvdevAxis.Y); + + is_valid &= Evdev.GetName(fd, out name) >= 0; + is_valid &= Evdev.GetId(fd, out id) >= 0; + + if (is_valid) { - break; + stick = new LinuxJoystickDetails + { + FileDescriptor = fd, + PathIndex = number, + State = new JoystickState(), + Name = name, + Guid = CreateGuid(id, name), + }; + stick.Caps = new JoystickCapabilities( + AddAxes(stick, axisbit, axissize), + AddButtons(stick, keybit, keysize), + AddHats(stick, axisbit, axissize, keybit, keysize), + true); + stick.State.SetIsConnected(true); } } - // If no disconnected joystick exists, append a new slot - if (i == sticks.Count) - { - sticks.Add(stick); - } - else - { - sticks[i] = stick; - } - - // Map player index to joystick - index_to_stick.Add(index_to_stick.Count, i); - Debug.Print("Found joystick on path {0}", path); } + catch (Exception e) + { + Debug.Print("Error opening joystick: {0}", e.ToString()); + } finally { if (stick == null && fd != -1) + { + // Not a joystick Libc.close(fd); + } } } return stick; } - void CloseJoystick(JoystickDevice js) + void CloseJoystick(LinuxJoystickDetails js) { - Libc.close(js.Details.FileDescriptor); - js.Details.State = new JoystickState(); // clear joystick state - js.Details.FileDescriptor = -1; - - // find and remove the joystick index from index_to_stick - int key = -1; - foreach (int i in index_to_stick.Keys) - { - if (sticks[index_to_stick[i]] == js) - { - key = i; - break; - } - } + Sticks.Remove(js.FileDescriptor); - if (index_to_stick.ContainsKey(key)) - { - index_to_stick.Remove(key); - } + Libc.close(js.FileDescriptor); + js.FileDescriptor = -1; + js.State = new JoystickState(); // clear joystick state + js.Caps = new JoystickCapabilities(); } - void PollJoystick(JoystickDevice js) + void PollJoystick(LinuxJoystickDetails js) { - JoystickEvent e; - unsafe { - while ((long)Libc.read(js.Details.FileDescriptor, (void*)&e, (UIntPtr)sizeof(JoystickEvent)) > 0) + const int EventCount = 32; + InputEvent* events = stackalloc InputEvent[EventCount]; + + long length = 0; + while (true) { - e.Type &= ~JoystickEventType.Init; + length = (long)Libc.read(js.FileDescriptor, (void*)events, (UIntPtr)(sizeof(InputEvent) * EventCount)); + if (length <= 0) + break; - switch (e.Type) + length /= sizeof(InputEvent); + for (int i = 0; i < length; i++) { - case JoystickEventType.Axis: - // Flip vertical axes so that +1 point up. - if (e.Number % 2 == 0) - js.Details.State.SetAxis((JoystickAxis)e.Number, e.Value); - else - js.Details.State.SetAxis((JoystickAxis)e.Number, unchecked((short)-e.Value)); - break; + InputEvent *e = events + i; + switch (e->Type) + { + case EvdevType.ABS: + break; - case JoystickEventType.Button: - js.Details.State.SetButton((JoystickButton)e.Number, e.Value != 0); - break; + case EvdevType.KEY: + break; + } + + //js.State.SetPacketNumber(unchecked((int)e->Time.Seconds)); } - - js.Details.State.SetPacketNumber(unchecked((int)e.Time)); } } } - bool IsValid(int index) - { - return index_to_stick.ContainsKey(index); - } - static readonly string JoystickPath = "/dev/input"; static readonly string JoystickPathLegacy = "/dev"; @@ -374,7 +415,7 @@ namespace OpenTK.Platform.Linux } watcher.Dispose(); - foreach (JoystickDevice js in sticks) + foreach (LinuxJoystickDetails js in Sticks) { CloseJoystick(js); } @@ -394,39 +435,33 @@ namespace OpenTK.Platform.Linux JoystickState IJoystickDriver2.GetState(int index) { - if (IsValid(index)) + LinuxJoystickDetails js = Sticks.FromIndex(index); + if (js != null) { - JoystickDevice js = - sticks[index_to_stick[index]]; PollJoystick(js); - return js.Details.State; + return js.State; } return new JoystickState(); } JoystickCapabilities IJoystickDriver2.GetCapabilities(int index) { - JoystickCapabilities caps = new JoystickCapabilities(); - if (IsValid(index)) + LinuxJoystickDetails js = Sticks.FromIndex(index); + if (js != null) { - JoystickDevice js = sticks[index_to_stick[index]]; - caps = new JoystickCapabilities( - js.Axis.Count, - js.Button.Count, - 0, // hats not supported by /dev/js - js.Details.State.IsConnected); + return js.Caps; } - return caps; + return new JoystickCapabilities(); } Guid IJoystickDriver2.GetGuid(int index) { - if (IsValid(index)) + LinuxJoystickDetails js = Sticks.FromIndex(index); + if (js != null) { - JoystickDevice js = sticks[index_to_stick[index]]; - return js.Details.Guid; + return js.Guid; } - return new Guid(); + return Guid.Empty; } #endregion From 129fb812241f1898cb5c6cf37dc1949188587246 Mon Sep 17 00:00:00 2001 From: thefiddler Date: Sat, 9 Aug 2014 21:26:21 +0200 Subject: [PATCH 2/5] [Linux] Implemented evdev joystick polling --- .../OpenTK/Platform/Linux/Bindings/Evdev.cs | 28 +++ Source/OpenTK/Platform/Linux/LinuxJoystick.cs | 200 +++++++++++------- 2 files changed, 154 insertions(+), 74 deletions(-) diff --git a/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs b/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs index e37cce79..15073721 100644 --- a/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs +++ b/Source/OpenTK/Platform/Linux/Bindings/Evdev.cs @@ -381,6 +381,23 @@ namespace OpenTK.Platform.Linux return (uint)v; } + // Get absolute value / limits + public static int GetAbs(int fd, EvdevAxis axis, out InputAbsInfo info) + { + info = default(InputAbsInfo); + unsafe + { + fixed (InputAbsInfo* pinfo = &info) + { + // EVIOCGABS(abs) = _IOR('E', 0x40 + (abs), struct input_absinfo) + uint ioctl = IOCreate(DirectionFlags.Read, (int)axis + 0x40, BlittableValueType.Stride); + int retval = Libc.ioctl(fd, ioctl, new IntPtr(pinfo)); + return retval; + } + } + } + + // Get supported event bits public static int GetBit(int fd, EvdevType ev, int length, IntPtr data) { // EVIOCGBIT = _IOC(_IOC_READ, 'E', 0x20 + (ev), len) @@ -577,6 +594,17 @@ namespace OpenTK.Platform.Linux Name128 = (2u << 30) | ((byte)'E' << 8) | (0x06u << 0) | (128u << 16), //EVIOCGNAME(len) = _IOC(_IOC_READ, 'E', 0x06, len) } + [StructLayout(LayoutKind.Sequential)] + struct InputAbsInfo + { + public int Value; + public int Minimum; + public int Maximum; + public int Fuzz; + public int Flat; + public int Resolution; + }; + [StructLayout(LayoutKind.Sequential)] struct InputId { diff --git a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs index 39a2f884..fcd621fe 100644 --- a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs +++ b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs @@ -35,6 +35,12 @@ using OpenTK.Input; namespace OpenTK.Platform.Linux { + struct AxisInfo + { + public JoystickAxis Axis; + public InputAbsInfo Info; + } + class LinuxJoystickDetails { public Guid Guid; @@ -44,18 +50,23 @@ namespace OpenTK.Platform.Linux public JoystickState State; public JoystickCapabilities Caps; - public readonly Dictionary AxisMap = - new Dictionary(); + public readonly Dictionary AxisMap = + new Dictionary(); public readonly Dictionary ButtonMap = new Dictionary(); - public readonly Dictionary HatMap = - new Dictionary(); } sealed class LinuxJoystick : IJoystickDriver2 { #region Fields + static readonly HatPosition[,] HatPositions = new HatPosition[,] + { + { HatPosition.UpLeft, HatPosition.Up, HatPosition.UpRight }, + { HatPosition.Left, HatPosition.Centered, HatPosition.Right }, + { HatPosition.DownLeft, HatPosition.Down, HatPosition.DownRight } + }; + readonly object sync = new object(); readonly FileSystemWatcher watcher = new FileSystemWatcher(); @@ -141,7 +152,6 @@ namespace OpenTK.Platform.Linux if (stick != null) { CloseJoystick(stick); - Sticks.TryRemove(number); } } } @@ -198,77 +208,53 @@ namespace OpenTK.Platform.Linux return (*(ptr + byte_offset) & (1 << bit_offset)) != 0; } - unsafe static int AddAxes(LinuxJoystickDetails stick, byte* axisbit, int bytecount) + unsafe static void QueryCapabilities(LinuxJoystickDetails stick, + byte* axisbit, int axisbytes, + byte* keybit, int keybytes, + out int axes, out int buttons, out int hats) { - JoystickAxis axes = 0; - JoystickHat hats = 0; - int bitcount = bytecount * 8; - for (EvdevAxis axis = 0; axis < EvdevAxis.CNT && (int)axis < bitcount; axis++) - { - if (axis >= EvdevAxis.HAT0X && axis <= EvdevAxis.HAT3Y) - { - // Axis is analogue hat - skip - continue; - } + // Note: since we are trying to be compatible with the SDL2 gamepad database, + // we have to match SDL2 behavior bug-for-bug. This means: + // HAT0-HAT3 are all reported as hats (even if the docs say that HAT1 and HAT2 can be analogue triggers) + // DPAD buttons are reported as buttons, not as hats (unlike Windows and Mac OS X) - if (TestBit(axisbit, (int)axis)) + axes = buttons = hats = 0; + for (EvdevAxis axis = 0; axis < EvdevAxis.CNT && (int)axis < axisbytes * 8; axis++) + { + InputAbsInfo info; + bool is_valid = true; + is_valid &= TestBit(axisbit, (int)axis); + is_valid &= Evdev.GetAbs(stick.FileDescriptor, axis, out info) >= 0; + if (is_valid) { - stick.AxisMap.Add(axis, axes++); - } - else - { - stick.AxisMap.Add(axis, (JoystickAxis)(-1)); + if (axis >= EvdevAxis.HAT0X && axis <= EvdevAxis.HAT3Y) + { + // Analogue hat + stick.AxisMap.Add(axis, new AxisInfo + { + Axis = (JoystickAxis)(JoystickHat)hats++, + Info = info + }); + } + else + { + // Regular axis + stick.AxisMap.Add(axis, new AxisInfo + { + Axis = (JoystickAxis)axes++, + Info = info + }); + } } } - return (int)axes; - } - unsafe static int AddButtons(LinuxJoystickDetails stick, byte* keybit, int bytecount) - { - JoystickButton buttons = 0; - int bitcount = bytecount * 8; - for (EvdevButton button = 0; button < EvdevButton.Last && (int)button < bitcount; button++) + for (EvdevButton button = 0; button < EvdevButton.Last && (int)button < keybytes * 8; button++) { - if (button >= EvdevButton.DPAD_UP && button <= EvdevButton.DPAD_RIGHT) - { - // Button is dpad (hat) - skip - continue; - } - if (TestBit(keybit, (int)button)) { - stick.ButtonMap.Add(button, buttons++); - } - else - { - stick.ButtonMap.Add(button, (JoystickButton)(-1)); + stick.ButtonMap.Add(button, (JoystickButton)buttons++); } } - return (int)buttons; - } - - unsafe static int AddHats(LinuxJoystickDetails stick, - byte* axisbit, int axiscount, - byte* keybit, int keycount) - { - JoystickHat hats = 0; - for (EvdevAxis hat = EvdevAxis.HAT0X; hat < EvdevAxis.HAT3Y && (int)hat < axiscount * 8; hat++) - { - if (TestBit(axisbit, (int)hat)) - { - stick.HatMap.Add((int)hat, hats++); - } - } - - for (EvdevButton dpad = EvdevButton.DPAD_UP; dpad < EvdevButton.DPAD_RIGHT && (int)dpad < keycount * 8; dpad++) - { - if (TestBit(axisbit, (int)dpad)) - { - stick.HatMap.Add((int)dpad, hats++); - } - } - - return (int)hats; } LinuxJoystickDetails OpenJoystick(string path) @@ -322,12 +308,15 @@ namespace OpenTK.Platform.Linux Name = name, Guid = CreateGuid(id, name), }; - stick.Caps = new JoystickCapabilities( - AddAxes(stick, axisbit, axissize), - AddButtons(stick, keybit, keysize), - AddHats(stick, axisbit, axissize, keybit, keysize), - true); - stick.State.SetIsConnected(true); + + int axes, buttons, hats; + QueryCapabilities(stick, axisbit, axissize, keybit, keysize, + out axes, out buttons, out hats); + + stick.Caps = new JoystickCapabilities(axes, buttons, hats, true); + + // Poll the joystick once, to initialize its state + PollJoystick(stick); } } @@ -360,6 +349,11 @@ namespace OpenTK.Platform.Linux js.Caps = new JoystickCapabilities(); } + JoystickHatState TranslateHat(int x, int y) + { + return new JoystickHatState(HatPositions[x, y]); + } + void PollJoystick(LinuxJoystickDetails js) { unsafe @@ -381,13 +375,71 @@ namespace OpenTK.Platform.Linux switch (e->Type) { case EvdevType.ABS: - break; + { + AxisInfo axis; + if (js.AxisMap.TryGetValue((EvdevAxis)e->Code, out axis)) + { + if (axis.Info.Maximum > axis.Info.Minimum) + { + if (e->Code >= (int)EvdevAxis.HAT0X && e->Code <= (int)EvdevAxis.HAT3Y) + { + // We currently treat analogue hats as digital hats + // to maintain compatibility with SDL2. We can do + // better than this, however. + JoystickHat hat = JoystickHat.Hat0 + (e->Code - (int)EvdevAxis.HAT0X) / 2; + JoystickHatState pos = js.State.GetHat(hat); + int xy_axis = (int)axis.Axis & 0x1; + switch (xy_axis) + { + case 0: + // X-axis + pos = TranslateHat( + e->Value.CompareTo(0) + 1, + pos.IsUp ? 0 : pos.IsDown ? 2 : 1); + break; + + case 1: + // Y-axis + pos = TranslateHat( + pos.IsLeft ? 0 : pos.IsRight ? 2 : 1, + e->Value.CompareTo(0) + 1); + break; + } + + js.State.SetHat(hat, pos); + } + else + { + // This axis represents a regular axis or trigger + js.State.SetAxis( + axis.Axis, + (short)Common.HidHelper.ScaleValue(e->Value, + axis.Info.Minimum, axis.Info.Maximum, + short.MinValue, short.MaxValue)); + } + } + } + break; + } case EvdevType.KEY: - break; + { + JoystickButton button; + if (js.ButtonMap.TryGetValue((EvdevButton)e->Code, out button)) + { + js.State.SetButton(button, e->Value != 0); + } + break; + } } - //js.State.SetPacketNumber(unchecked((int)e->Time.Seconds)); + // Create a serial number (total seconds in 24.8 fixed point format) + int sec = (int)((long)e->Time.Seconds & 0xffffffff); + int msec = (int)e->Time.MicroSeconds / 1000; + int packet = + ((sec & 0x00ffffff) << 24) | + Common.HidHelper.ScaleValue(msec, 0, 1000, 0, 255); + js.State.SetPacketNumber(unchecked((int)e->Time.Seconds)); } } } From 21deea87d835930586bfdce7b7f8aa424af93a83 Mon Sep 17 00:00:00 2001 From: thefiddler Date: Sun, 10 Aug 2014 03:01:49 +0200 Subject: [PATCH 3/5] [Linux] Correctly set evdev packet number --- Source/OpenTK/Platform/Linux/LinuxJoystick.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs index fcd621fe..a2d61a87 100644 --- a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs +++ b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs @@ -439,7 +439,7 @@ namespace OpenTK.Platform.Linux int packet = ((sec & 0x00ffffff) << 24) | Common.HidHelper.ScaleValue(msec, 0, 1000, 0, 255); - js.State.SetPacketNumber(unchecked((int)e->Time.Seconds)); + js.State.SetPacketNumber(packet); } } } From 3bf29b074cc47d824adc49701c65ef08303f030c Mon Sep 17 00:00:00 2001 From: thefiddler Date: Mon, 11 Aug 2014 08:58:34 +0200 Subject: [PATCH 4/5] [Input] Fixed debug display of Back gamepad button --- Source/OpenTK/Input/GamePadButtons.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/OpenTK/Input/GamePadButtons.cs b/Source/OpenTK/Input/GamePadButtons.cs index 3f866e89..3910d2e1 100644 --- a/Source/OpenTK/Input/GamePadButtons.cs +++ b/Source/OpenTK/Input/GamePadButtons.cs @@ -175,6 +175,8 @@ namespace OpenTK.Input sb.Append("St"); if (BigButton == ButtonState.Pressed) sb.Append("Gd"); + if (Back == ButtonState.Pressed) + sb.Append("Bk"); if (LeftShoulder == ButtonState.Pressed) sb.Append("L"); if (RightShoulder == ButtonState.Pressed) From 179c82387c97cd7fe7d3c7871d6de7b5cd7a974f Mon Sep 17 00:00:00 2001 From: thefiddler Date: Mon, 11 Aug 2014 08:58:57 +0200 Subject: [PATCH 5/5] [Linux] Fixed byteswapping in joystick Guid calculation --- Source/OpenTK/Platform/Linux/LinuxJoystick.cs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs index a2d61a87..348e3e3c 100644 --- a/Source/OpenTK/Platform/Linux/LinuxJoystick.cs +++ b/Source/OpenTK/Platform/Linux/LinuxJoystick.cs @@ -163,29 +163,35 @@ namespace OpenTK.Platform.Linux Guid CreateGuid(EvdevInputId id, string name) { + // Note: the first 8bytes of the Guid are byteswapped + // in three parts when using `new Guid(byte[])`: + // (int, short, short). + // We need to take that into account to match the expected + // Guid in the database. Ugh. + byte[] bytes = new byte[16]; int i = 0; - byte[] bus = BitConverter.GetBytes(id.BusType); - bytes[i++] = bus[0]; + byte[] bus = BitConverter.GetBytes((int)id.BusType); + bytes[i++] = bus[3]; + bytes[i++] = bus[2]; bytes[i++] = bus[1]; - bytes[i++] = 0; - bytes[i++] = 0; + bytes[i++] = bus[0]; if (id.Vendor != 0 && id.Product != 0 && id.Version != 0) { byte[] vendor = BitConverter.GetBytes(id.Vendor); byte[] product = BitConverter.GetBytes(id.Product); byte[] version = BitConverter.GetBytes(id.Version); - bytes[i++] = vendor[0]; bytes[i++] = vendor[1]; + bytes[i++] = vendor[0]; bytes[i++] = 0; bytes[i++] = 0; - bytes[i++] = product[0]; + bytes[i++] = product[0]; // no byteswapping bytes[i++] = product[1]; bytes[i++] = 0; bytes[i++] = 0; - bytes[i++] = version[0]; + bytes[i++] = version[0]; // no byteswapping bytes[i++] = version[1]; bytes[i++] = 0; bytes[i++] = 0; @@ -313,7 +319,7 @@ namespace OpenTK.Platform.Linux QueryCapabilities(stick, axisbit, axissize, keybit, keysize, out axes, out buttons, out hats); - stick.Caps = new JoystickCapabilities(axes, buttons, hats, true); + stick.Caps = new JoystickCapabilities(axes, buttons, hats, false); // Poll the joystick once, to initialize its state PollJoystick(stick); @@ -368,6 +374,12 @@ namespace OpenTK.Platform.Linux if (length <= 0) break; + // Only mark the joystick as connected when we actually start receiving events. + // Otherwise, the Xbox wireless receiver will register 4 joysticks even if no + // actual joystick is connected to the receiver. + js.Caps.SetIsConnected(true); + js.State.SetIsConnected(true); + length /= sizeof(InputEvent); for (int i = 0; i < length; i++) {