| 1 | // tControllerSystem.cpp  |
| 2 | //  |
| 3 | // This file implements the main API for the input system. It manages all attached controllers.  |
| 4 | //  |
| 5 | // Copyright (c) 2025 Tristan Grimmer.  |
| 6 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 7 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 8 | //  |
| 9 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 10 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 11 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 12 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 13 | // PERFORMANCE OF THIS SOFTWARE.  |
| 14 |   |
| 15 | #include <chrono>  |
| 16 | #ifdef PLATFORM_WINDOWS  |
| 17 | #include <windows.h>  |
| 18 | #include <xinput.h>  |
| 19 | #endif  |
| 20 | #include <Foundation/tPlatform.h>  |
| 21 | #include <Foundation/tName.h>  |
| 22 | #include <System/tPrint.h>  |
| 23 | #include "Input/tControllerSystem.h"  |
| 24 | #include "Input/tControllerDefinitions.h"  |
| 25 |   |
| 26 |   |
| 27 | #ifdef PLATFORM_WINDOWS  |
| 28 | // Missing from xinput.h.  |
| 29 | _XInputGetCapabilitiesEx XInputGetCapabilitiesEx;  |
| 30 | #endif  |
| 31 |   |
| 32 |   |
| 33 | namespace tInput  |
| 34 | {  |
| 35 |   |
| 36 |   |
| 37 | tControllerSystem::tControllerSystem(int pollingPeriod_ns, int detectionPeriod_ms) :  |
| 38 | PollingPeriod_ns(pollingPeriod_ns)  |
| 39 | {  |
| 40 | Gamepads.reserve(n: int(tGamepadID::NumGamepads));  |
| 41 | Gamepads.emplace_back(args: "Gamepad1" , args: tGamepadID::GP0);  |
| 42 | Gamepads.emplace_back(args: "Gamepad2" , args: tGamepadID::GP1);  |
| 43 | Gamepads.emplace_back(args: "Gamepad3" , args: tGamepadID::GP2);  |
| 44 | Gamepads.emplace_back(args: "Gamepad4" , args: tGamepadID::GP3);  |
| 45 |   |
| 46 | DetectPeriod_ms = (detectionPeriod_ms > 0) ? detectionPeriod_ms : 1000;  |
| 47 |   |
| 48 | #ifdef PLATFORM_WINDOWS  |
| 49 | // This is better than LoadLibrary(TEXT("XInput1_4.dll") in two ways:  |
| 50 | // 1) The module is already loaded using the import library.  |
| 51 | // 2) We use the xinput.h header define in case xinput is ever updated.  |
| 52 | HMODULE moduleHandle = GetModuleHandle(XINPUT_DLL);  |
| 53 | XInputGetCapabilitiesEx = (_XInputGetCapabilitiesEx)GetProcAddress(moduleHandle, (char*)108);  |
| 54 | #endif  |
| 55 |   |
| 56 | DetectThread = std::thread(&tControllerSystem::Detect, this);  |
| 57 | }  |
| 58 |   |
| 59 |   |
| 60 | tControllerSystem::~tControllerSystem()  |
| 61 | {  |
| 62 | {  |
| 63 | std::lock_guard<std::mutex> lock(Mutex);  |
| 64 | DetectExitRequested = true;  |
| 65 | }  |
| 66 | // Notify that we want to cooperatively stop the polling thread. Notify one (thread) should be sufficient.  |
| 67 | // Notify_all would also work but is overkill since only one (polling) thread is waiting. By using a condition  |
| 68 | // variable we've made it so we don't have to wait for the current polling cycle sleep to complete.  |
| 69 | DetectExitCondition.notify_one();  |
| 70 |   |
| 71 | // The join just blocks until the polling thread has finished -- and has responded to the notify above.  |
| 72 | DetectThread.join();  |
| 73 | }  |
| 74 |   |
| 75 |   |
| 76 | void tControllerSystem::Detect()  |
| 77 | {  |
| 78 | while (1)  |
| 79 | {  |
| 80 | // Detect controllers connected and disconnected.  |
| 81 | #ifdef PLATFORM_WINDOWS  |
| 82 |   |
| 83 | for (int g = 0; g < int(tGamepadID::NumGamepads); g++)  |
| 84 | {  |
| 85 | // XInputGetState is generally faster for detecting device connectedness.  |
| 86 | WinXInputState state;  |
| 87 | tStd::tMemclr(&state, sizeof(WinXInputState));  |
| 88 | WinDWord result = XInputGetState(g, &state);  |
| 89 |   |
| 90 | if (result == WinErrorSuccess)  |
| 91 | {  |
| 92 | // If we're already polling then move on.  |
| 93 | if (Gamepads[g].IsPolling())  |
| 94 | continue;  |
| 95 |   |
| 96 | // Controller connected. Now we need to start polling the controller and queue a message that a  |
| 97 | // controller has been connected for the main Update to pick up. If polling period is set to 0 the  |
| 98 | // gamepad will do a hrdware lookup to determine the polling period.  |
| 99 | Gamepads[g].StartPolling(PollingPeriod_ns);  |
| 100 | }  |
| 101 | else  |
| 102 | {  |
| 103 | // Either WinErrorDeviceNotConnected or some other error. Either way treat controller as disconnected.  |
| 104 | if (Gamepads[g].IsPolling())  |
| 105 | {  |
| 106 | // Now we need to stop polling and queue a disconnect message to for the main update to pick up.  |
| 107 | // Since the gamepad is now disconnected  |
| 108 | Gamepads[g].StopPolling();  |
| 109 | }  |
| 110 | }  |
| 111 | }  |
| 112 | #endif  |
| 113 |   |
| 114 | static int detectNum = 0;  |
| 115 | tPrintf(f: "Detect: %d\n" , detectNum++);  |
| 116 |   |
| 117 | // This unique_lock is just a more powerful version of lock_guard. Supports subsequent unlocking/locking which  |
| 118 | // is presumably needed by wait_for. In any case, wait_for needs this type of lock.  |
| 119 | std::unique_lock<std::mutex> lock(Mutex);  |
| 120 | bool exitRequested = DetectExitCondition.wait_for(lock&: lock, rtime: std::chrono::milliseconds(DetectPeriod_ms), p: [this]{ return DetectExitRequested; });  |
| 121 | if (exitRequested)  |
| 122 | break;  |
| 123 | }  |
| 124 | }  |
| 125 |   |
| 126 |   |
| 127 | void tControllerSystem::Update()  |
| 128 | {  |
| 129 | for (int g = 0; g < int(tGamepadID::NumGamepads); g++)  |
| 130 | {  |
| 131 | Gamepads[g].Update();  |
| 132 | }  |
| 133 | }  |
| 134 |   |
| 135 |   |
| 136 | }  |
| 137 | |