1 /// Translated from C to D
2 module glfw3.linux_joystick;
3 
4 extern(C): @nogc: nothrow: __gshared:
5 
6 //========================================================================
7 // GLFW 3.3 Linux - www.glfw.org
8 //------------------------------------------------------------------------
9 // Copyright (c) 2002-2006 Marcus Geelnard
10 // Copyright (c) 2006-2017 Camilla Löwy <elmindreda@glfw.org>
11 //
12 // This software is provided 'as-is', without any express or implied
13 // warranty. In no event will the authors be held liable for any damages
14 // arising from the use of this software.
15 //
16 // Permission is granted to anyone to use this software for any purpose,
17 // including commercial applications, and to alter it and redistribute it
18 // freely, subject to the following restrictions:
19 //
20 // 1. The origin of this software must not be misrepresented; you must not
21 //    claim that you wrote the original software. If you use this software
22 //    in a product, an acknowledgment in the product documentation would
23 //    be appreciated but is not required.
24 //
25 // 2. Altered source versions must be plainly marked as such, and must not
26 //    be misrepresented as being the original software.
27 //
28 // 3. This notice may not be removed or altered from any source
29 //    distribution.
30 //
31 //========================================================================
32 // It is fine to use C99 in this file because it will not be built with VS
33 //========================================================================
34 
35 import glfw3.internal;
36 
37 import core.sys.posix.sys.types;
38 import core.sys.posix.sys.stat;
39 import core.sys.linux.sys.inotify;
40 import core.sys.posix.fcntl;
41 import core.stdc.errno;
42 import core.sys.posix.dirent;
43 import core.stdc.stdio;
44 import core.stdc.stdlib;
45 import core.stdc.string;
46 import core.sys.posix.unistd;
47 
48 public import glfw3.linuxinput;
49 import core.stdc.limits;
50 
51 import core.sys.posix.sys.ioctl: ioctl;
52 
53 mixin template _GLFW_PLATFORM_JOYSTICK_STATE() {        _GLFWjoystickLinux linjs; }
54 mixin template _GLFW_PLATFORM_LIBRARY_JOYSTICK_STATE() {_GLFWlibraryLinux  linjs; }
55 
56 enum _GLFW_PLATFORM_MAPPING_NAME = "Linux";
57 
58 // Linux-specific joystick data
59 //
60 struct _GLFWjoystickLinux {
61     int fd;
62     char[PATH_MAX] path = '\0';
63     int[KEY_CNT - BTN_MISC] keyMap;
64     int[ABS_CNT] absMap;
65     input_absinfo[ABS_CNT] absInfo;
66     int[2][4] hats;
67     bool hasRumble = false;
68     ff_effect rumble;
69 }
70 
71 // Linux-specific joystick API data
72 //
73 struct _GLFWlibraryLinux {
74     int inotify;
75     int watch;
76     version(none) {
77         regex_t regex;
78     }
79     GLFWbool dropped;
80 }
81 
82 version(none) { // < v2.6.39 kernel headers
83     // Workaround for CentOS-6, which is supported till 2020-11-30, but still on v2.6.32
84     enum SYN_DROPPED = 3;
85 }
86 
87 // Apply an EV_KEY event to the specified joystick
88 //
89 private void handleKeyEvent(_GLFWjoystick* js, int code, int value) {
90     _glfwInputJoystickButton(js,
91                              js.linjs.keyMap[code - BTN_MISC],
92                              value ? GLFW_PRESS : GLFW_RELEASE);
93 }
94 
95 // Apply an EV_ABS event to the specified joystick
96 //
97 private void handleAbsEvent(_GLFWjoystick* js, int code, int value) {
98     const(int) index = js.linjs.absMap[code];
99 
100     if (code >= ABS_HAT0X && code <= ABS_HAT3Y)
101     {
102         static const(char)[3][3] stateMap = [
103             [ GLFW_HAT_CENTERED, GLFW_HAT_UP,       GLFW_HAT_DOWN ],
104             [ GLFW_HAT_LEFT,     GLFW_HAT_LEFT_UP,  GLFW_HAT_LEFT_DOWN ],
105             [ GLFW_HAT_RIGHT,    GLFW_HAT_RIGHT_UP, GLFW_HAT_RIGHT_DOWN ],
106         ];
107 
108         const(int) hat = (code - ABS_HAT0X) / 2;
109         const(int) axis = (code - ABS_HAT0X) % 2;
110         int* state = js.linjs.hats[hat].ptr;
111 
112         // NOTE: Looking at several input drivers, it seems all hat events use
113         //       -1 for left / up, 0 for centered and 1 for right / down
114         if (value == 0)
115             state[axis] = 0;
116         else if (value < 0)
117             state[axis] = 1;
118         else if (value > 0)
119             state[axis] = 2;
120 
121         _glfwInputJoystickHat(js, index, stateMap[state[0]][state[1]]);
122     }
123     else
124     {
125         input_absinfo* info = &js.linjs.absInfo[code];
126         float normalized = value;
127 
128         const(int) range = info.maximum - info.minimum;
129         if (range)
130         {
131             // Normalize to 0.0 -> 1.0
132             normalized = (normalized - info.minimum) / range;
133             // Normalize to -1.0 -> 1.0
134             normalized = normalized * 2.0f - 1.0f;
135         }
136 
137         _glfwInputJoystickAxis(js, index, normalized);
138     }
139 }
140 
141 // Poll state of absolute axes
142 //
143 private void pollAbsState(_GLFWjoystick* js) {
144     for (int code = 0;  code < ABS_CNT;  code++)
145     {
146         if (js.linjs.absMap[code] < 0)
147             continue;
148 
149         input_absinfo* info = &js.linjs.absInfo[code];
150 
151         if (ioctl(js.linjs.fd, EVIOCGABS(code), info) < 0)
152             continue;
153 
154         handleAbsEvent(js, code, info.value);
155     }
156 }
157 
158 // #define isBitSet(bit, arr) (arr[(bit) / 8] & (1 << ((bit) % 8)))
159 // enum string isBitSet(string bit, string arr) = ` (`~arr~`[(`~bit~`) / 8] & (1 << ((`~bit~`) % 8)))`;
160 private bool isBitSet(int bit, scope const ubyte[] arr) {return cast(bool) (arr[bit / 8] & (1 << (bit % 8)));}
161 
162 private void initJoystickForceFeedback(_GLFWjoystickLinux *linjs)
163 {
164     linjs.hasRumble = false;
165 
166     ubyte[(FF_CNT + 7) / 8] ffBits = 0;
167     if (ioctl(linjs.fd, EVIOCGBIT!(typeof(ffBits))(EV_FF), ffBits.ptr) < 0)
168     {
169         return;
170     }
171 
172     if (isBitSet(FF_RUMBLE, ffBits))
173     {
174         linjs.rumble.type =      FF_RUMBLE;
175         linjs.rumble.id =        -1;
176         linjs.rumble.direction = 0;
177         linjs.rumble.trigger = ff_trigger(/*.button*/ 0, /*.interval*/ 0);
178         linjs.rumble.replay = ff_replay(/*length*/ 2000, /*delay*/ 0);
179         linjs.rumble.u.rumble = ff_rumble_effect(/*strong_magnitude*/ 0, /*weak_magnitude*/ 0); // xinput rumble lasts ~2 seconds
180 
181         linjs.hasRumble = (ioctl(linjs.fd, EVIOCSFF, &linjs.rumble) >= 0);
182     }
183 }
184 
185 // Attempt to open the specified joystick device
186 //
187 private GLFWbool openJoystickDevice(const(char)* path) {
188     for (int jid = 0;  jid <= GLFW_JOYSTICK_LAST;  jid++)
189     {
190         if (!_glfw.joysticks[jid].present)
191             continue;
192         if (strcmp(_glfw.joysticks[jid].linjs.path.ptr, path) == 0)
193             return GLFW_FALSE;
194     }
195 
196     _GLFWjoystickLinux linjs = _GLFWjoystickLinux(0);
197     linjs.fd = open(path, O_RDWR | O_NONBLOCK);
198     if (linjs.fd == -1)
199         return GLFW_FALSE;
200 
201     ubyte[(EV_CNT + 7) / 8] evBits = 0;
202     ubyte[(KEY_CNT + 7) / 8] keyBits = 0;
203     ubyte[(ABS_CNT + 7) / 8] absBits = 0;
204     input_id id;
205 
206     if (ioctl(linjs.fd, EVIOCGBIT!(typeof(evBits) )(     0), evBits.ptr) < 0 ||
207         ioctl(linjs.fd, EVIOCGBIT!(typeof(keyBits))(EV_KEY), keyBits.ptr) < 0 ||
208         ioctl(linjs.fd, EVIOCGBIT!(typeof(absBits))(EV_ABS), absBits.ptr) < 0 ||
209         ioctl(linjs.fd, EVIOCGID, &id) < 0)
210     {
211         _glfwInputError(GLFW_PLATFORM_ERROR,
212                         "Linux: Failed to query input device: %s",
213                         strerror(errno));
214         close(linjs.fd);
215         return GLFW_FALSE;
216     }
217 
218     // Ensure this device supports the events expected of a joystick
219     if (!isBitSet(EV_KEY, evBits) || !isBitSet(EV_ABS, evBits))
220     {
221         close(linjs.fd);
222         return GLFW_FALSE;
223     }
224 
225     char[256] name = "";
226 
227     if (ioctl(linjs.fd, EVIOCGNAME!(typeof(name))(), name.ptr) < 0)
228         strncpy(name.ptr, "Unknown", name.length);
229 
230     char[33] guid = "";
231 
232     // Generate a joystick GUID that matches the SDL 2.0.5+ one
233     if (id.vendor && id.product && id.version_)
234     {
235         sprintf(guid.ptr, "%02x%02x0000%02x%02x0000%02x%02x0000%02x%02x0000",
236                 id.bustype & 0xff, id.bustype >> 8,
237                 id.vendor & 0xff,  id.vendor >> 8,
238                 id.product & 0xff, id.product >> 8,
239                 id.version_ & 0xff, id.version_ >> 8);
240     }
241     else
242     {
243         sprintf(guid.ptr, "%02x%02x0000%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x00",
244                 id.bustype & 0xff, id.bustype >> 8,
245                 name[0], name[1], name[2], name[3],
246                 name[4], name[5], name[6], name[7],
247                 name[8], name[9], name[10]);
248     }
249 
250     int axisCount = 0;int buttonCount = 0;int hatCount = 0;
251 
252     for (int code = BTN_MISC;  code < KEY_CNT;  code++)
253     {
254         if (!isBitSet(code, keyBits))
255             continue;
256 
257         linjs.keyMap[code - BTN_MISC] = buttonCount;
258         buttonCount++;
259     }
260 
261     for (int code = 0;  code < ABS_CNT;  code++)
262     {
263         linjs.absMap[code] = -1;
264         if (!isBitSet(code, absBits))
265             continue;
266 
267         if (code >= ABS_HAT0X && code <= ABS_HAT3Y)
268         {
269             linjs.absMap[code] = hatCount;
270             hatCount++;
271             // Skip the Y axis
272             code++;
273         }
274         else
275         {
276             if (ioctl(linjs.fd, EVIOCGABS(code), &linjs.absInfo[code]) < 0)
277                 continue;
278 
279             linjs.absMap[code] = axisCount;
280             axisCount++;
281         }
282     }
283 
284     initJoystickForceFeedback(&linjs);
285 
286     _GLFWjoystick* js = _glfwAllocJoystick(name.ptr, guid.ptr, axisCount, buttonCount, hatCount);
287     if (!js)
288     {
289         close(linjs.fd);
290         return GLFW_FALSE;
291     }
292 
293     strncpy(linjs.path.ptr, path, linjs.path.length - 1);
294     memcpy(&js.linjs, &linjs, linjs.sizeof); // twab: wrong size, linjs.path.length
295 
296     pollAbsState(js);
297 
298     _glfwInputJoystick(js, GLFW_CONNECTED);
299     return GLFW_TRUE;
300 }
301 
302 // Frees all resources associated with the specified joystick
303 //
304 private void closeJoystick(_GLFWjoystick* js) {
305     close(js.linjs.fd);
306     _glfwFreeJoystick(js);
307     _glfwInputJoystick(js, GLFW_DISCONNECTED);
308 }
309 
310 // Lexically compare joysticks by name; used by qsort
311 //
312 private int compareJoysticks(const(void)* fp, const(void)* sp) {
313     auto fj = cast(const(_GLFWjoystick)*) fp;
314     auto sj = cast(const(_GLFWjoystick)*) sp;
315     return strcmp(fj.linjs.path.ptr, sj.linjs.path.ptr);
316 }
317 
318 
319 //////////////////////////////////////////////////////////////////////////
320 //////                       GLFW internal API                      //////
321 //////////////////////////////////////////////////////////////////////////
322 
323 /// Returns: `true` if `str` matches the regex `^event[0-9]\\+$`
324 private extern(D) bool isEventFile(const(char)* str) {
325     import core.stdc.string: strlen;
326     const len = strlen(str);
327     if (len < "event0".length) {
328         return false;
329     }
330     if (str[0..5] != "event") {
331         return false;
332     }
333     foreach(i; 5..len) {
334         if (str[i] < '0' || str[i] > '9') {
335             return false;
336         }
337     }
338     return true;
339 }
340 
341 @("is event file") unittest {
342     assert(isEventFile("event0"));
343     assert(isEventFile("event1234567890"));
344     assert(!isEventFile("event"));
345     assert(!isEventFile("even0"));
346     assert(!isEventFile("event0A"));
347 }
348 
349 static if (__VERSION__ < 2094) {
350     // workaround for missing @nogc attribute in core.sys.linux.sys.inotify
351     // should be fixed in dmd 2.094
352     extern(C) nothrow @nogc int inotify_init();
353     extern(C) nothrow @nogc int inotify_init1(int flags);
354     extern(C) nothrow @nogc int inotify_add_watch(int fd, const(char)* name, uint mask);
355     extern(C) nothrow @nogc int inotify_rm_watch(int fd, uint wd);
356 }
357 
358 // Initialize joystick interface
359 //
360 GLFWbool _glfwInitJoysticksLinux() {
361     const(char)* dirname = "/dev/input";
362 
363     _glfw.linjs.inotify = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
364     if (_glfw.linjs.inotify > 0)
365     {
366         // HACK: Register for IN_ATTRIB to get notified when udev is done
367         //       This works well in practice but the true way is libudev
368 
369         _glfw.linjs.watch = inotify_add_watch(_glfw.linjs.inotify,
370                                               dirname,
371                                               IN_CREATE | IN_ATTRIB | IN_DELETE);
372     }
373 
374     // Continue without device connection notifications if inotify fails
375     version(none) {
376         // remove regex dependency
377         if (regcomp(&_glfw.linjs.regex, "^event[0-9]\\+$", 0) != 0)
378         {
379             _glfwInputError(GLFW_PLATFORM_ERROR, "Linux: Failed to compile regex");
380             return GLFW_FALSE;
381         }
382     }
383 
384     int count = 0;
385 
386     DIR* dir = opendir(dirname);
387     if (dir)
388     {
389         dirent* entry;
390 
391         while (true)
392         {
393             entry = readdir(dir);
394             if (!entry) {
395                 break;
396             }
397 
398             version(none) {
399                 regmatch_t match;
400                 if (regexec(&_glfw.linjs.regex, entry.d_name, 1, &match, 0) != 0)
401                     continue;
402             } else {
403                 // remove regex dependency
404                 if (!isEventFile(entry.d_name.ptr)) {
405                     continue;
406                 }
407             }
408 
409             char[PATH_MAX] path;
410 
411             snprintf(path.ptr, path.length, "%s/%s", dirname, entry.d_name.ptr);
412 
413             if (openJoystickDevice(path.ptr))
414                 count++;
415         }
416 
417         closedir(dir);
418     }
419 
420     // Continue with no joysticks if enumeration fails
421 
422     qsort(_glfw.joysticks.ptr, count, _GLFWjoystick.sizeof, &compareJoysticks);
423     return GLFW_TRUE;
424 }
425 
426 // Close all opened joystick handles
427 //
428 void _glfwTerminateJoysticksLinux() {
429     int jid;
430 
431     for (jid = 0;  jid <= GLFW_JOYSTICK_LAST;  jid++)
432     {
433         _GLFWjoystick* js = _glfw.joysticks.ptr + jid;
434         if (js.present)
435             closeJoystick(js);
436     }
437 
438     version(none) {
439         regfree(&_glfw.linjs.regex);
440     }
441 
442     if (_glfw.linjs.inotify > 0)
443     {
444         if (_glfw.linjs.watch > 0)
445             inotify_rm_watch(_glfw.linjs.inotify, _glfw.linjs.watch);
446 
447         close(_glfw.linjs.inotify);
448     }
449 }
450 
451 void _glfwDetectJoystickConnectionLinux() {
452     if (_glfw.linjs.inotify <= 0)
453         return;
454 
455     ssize_t offset = 0;
456     char[16384] buffer;
457     const(ssize_t) size = read(_glfw.linjs.inotify, buffer.ptr, typeof(buffer).sizeof);
458 
459     while (size > offset)
460     {
461         const(inotify_event)* e = cast(inotify_event*) (buffer.ptr + offset);
462 
463         offset += typeof(cast(inotify_event) + e.len).sizeof;
464 
465         version(none) {
466             regmatch_t match;
467             if (regexec(&_glfw.linjs.regex, e.name, 1, &match, 0) != 0)
468                 continue;
469         } else {
470             if (!isEventFile(e.name.ptr)) {
471                 continue;
472             }
473         }
474 
475         char[PATH_MAX] path;
476         snprintf(path.ptr, path.length, "/dev/input/%s", e.name.ptr);
477 
478         if (e.mask & (IN_CREATE | IN_ATTRIB))
479             openJoystickDevice(path.ptr);
480         else if (e.mask & IN_DELETE)
481         {
482             for (int jid = 0;  jid <= GLFW_JOYSTICK_LAST;  jid++)
483             {
484                 if (strcmp(_glfw.joysticks[jid].linjs.path.ptr, path.ptr) == 0)
485                 {
486                     closeJoystick(_glfw.joysticks.ptr + jid);
487                     break;
488                 }
489             }
490         }
491     }
492 }
493 
494 
495 //////////////////////////////////////////////////////////////////////////
496 //////                       GLFW platform API                      //////
497 //////////////////////////////////////////////////////////////////////////
498 
499 int _glfwPlatformPollJoystick(_GLFWjoystick* js, int mode) {
500     // Read all queued events (non-blocking)
501     for (;;)
502     {
503         input_event e;
504 
505         errno = 0;
506         if (read(js.linjs.fd, &e, typeof(e).sizeof) < 0)
507         {
508             // Reset the joystick slot if the device was disconnected
509             if (errno == ENODEV)
510                 closeJoystick(js);
511 
512             break;
513         }
514 
515         if (e.type == EV_SYN)
516         {
517             if (e.code == SYN_DROPPED)
518                 _glfw.linjs.dropped = GLFW_TRUE;
519             else if (e.code == SYN_REPORT)
520             {
521                 _glfw.linjs.dropped = GLFW_FALSE;
522                 pollAbsState(js);
523             }
524         }
525 
526         if (_glfw.linjs.dropped)
527             continue;
528 
529         if (e.type == EV_KEY)
530             handleKeyEvent(js, e.code, e.value);
531         else if (e.type == EV_ABS)
532             handleAbsEvent(js, e.code, e.value);
533     }
534 
535     return js.present;
536 }
537 
538 void _glfwPlatformUpdateGamepadGUID(char* guid) {
539 }
540 
541 int _glfwPlatformSetJoystickRumble(_GLFWjoystick* js, float slowMotorIntensity, float fastMotorIntensity)
542 {
543     _GLFWjoystickLinux *linjs = &js.linjs;
544 
545     if (!js.linjs.hasRumble)
546         return GLFW_FALSE;
547 
548     js.linjs.rumble.u.rumble = ff_rumble_effect(
549         /*strong_magnitude*/ cast(ushort) (65_535 * slowMotorIntensity),
550         /*weak_magnitude*/   cast(ushort) (65_535 * fastMotorIntensity)
551     );
552 
553     input_event play;
554     play.type = EV_FF;
555     play.code = linjs.rumble.id;
556     play.value = 1;
557 
558     if (ioctl(linjs.fd, EVIOCSFF, &linjs.rumble) < 0) {
559         return GLFW_FALSE;
560     }
561     if (write(linjs.fd, &play, play.sizeof) < 0) {
562         return GLFW_FALSE;
563     }
564 
565     return GLFW_TRUE;
566 }