| 1 | // tContGamepad.cpp  |
| 2 | //  |
| 3 | // This file implements a gamepad controller. Controllers represent physical input devices.  |
| 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 "Input/tContGamepad.h"  |
| 22 | #include "System/tPrint.h"  |
| 23 | namespace tInput  |
| 24 | {  |
| 25 |   |
| 26 |   |
| 27 | void tContGamepad::StartPolling(int pollingPeriod_ns)  |
| 28 | {  |
| 29 | // If it's already running do nothing. We also don't update the period if we're already running.  |
| 30 | if (IsPolling())  |
| 31 | return;  |
| 32 |   |
| 33 | // Set the controller definition.  |
| 34 | SetDefinition();  |
| 35 |   |
| 36 | // PollingPeriod is only ever set before the thread starts and doesn't change. We don't need to Mutex protect it.  |
| 37 | if (pollingPeriod_ns <= 0)  |
| 38 | PollingPeriod_ns = int(1000000.0f/Definition.MaxPollingFreq);  |
| 39 | else  |
| 40 | PollingPeriod_ns = pollingPeriod_ns;  |
| 41 |   |
| 42 | PollingThread = std::thread(&tContGamepad::Poll, this);  |
| 43 | }  |
| 44 |   |
| 45 |   |
| 46 | void tContGamepad::StopPolling()  |
| 47 | {  |
| 48 | // If it's already stopped so do nothing.  |
| 49 | if (!IsPolling())  |
| 50 | return;  |
| 51 |   |
| 52 | // We don't want to wait around to exit the polling thread to finish its sleep cycle so we use a condition_variable  |
| 53 | // that uses PollingExitRequested as the predecate.  |
| 54 | {  |
| 55 | const std::lock_guard<std::mutex> lock(Mutex);  |
| 56 | PollingExitRequested = true;  |
| 57 | }  |
| 58 |   |
| 59 | // Joins back up to the detection thread.  |
| 60 | PollingThread.join();  |
| 61 | ClearDefinition();  |
| 62 | }  |
| 63 |   |
| 64 |   |
| 65 | void tContGamepad::Poll()  |
| 66 | {  |
| 67 | while (true)  |
| 68 | {  |
| 69 | #ifdef PLATFORM_WINDOWS  |
| 70 | WinXInputState state;  |
| 71 | tStd::tMemclr(&state, sizeof(WinXInputState));  |
| 72 |   |
| 73 | // Get the state of the controller from XInput. From what I can tell reading different gamepads on different  |
| 74 | // threads does not require a mutex to protect this call. Only this thread instance will read this particular  |
| 75 | // controller and so two calls to XInputGetState for the same controller will never happed at the same time.  |
| 76 | WinDWord result = XInputGetState(int(GamepadID), &state);  |
| 77 |   |
| 78 | if (result == WinErrorSuccess)  |
| 79 | {  |
| 80 | // Controller connected. We can read its state and update components.  |
| 81 | if (state.dwPacketNumber != PollingPacketNumber)  |
| 82 | {  |
| 83 | // The set calls to the components below are all mutex protected internally. Main thread may read input  |
| 84 | // unit values so we require the protection.  |
| 85 |   |
| 86 | // Note there is a bit more precision for the neg integral values. -32768 to 32767 maps to [-1.0, 1.0]  |
| 87 | int16 rawLX = state.Gamepad.sThumbLX;  |
| 88 | float axisLXNorm = (rawLX < 0) ? float(rawLX)/32768.0f : float(rawLX)/32767.0f;  |
| 89 | tPrintf("LX: %05.3f " , axisLXNorm);  |
| 90 | // @todo Set LStick X.  |
| 91 |   |
| 92 | int16 rawLY = state.Gamepad.sThumbLY;  |
| 93 | float axisLYNorm = (rawLY < 0) ? float(rawLY)/32768.0f : float(rawLY)/32767.0f;  |
| 94 | tPrintf("LY: %05.3f " , axisLXNorm);  |
| 95 | // @todo Set LStick Y.  |
| 96 |   |
| 97 | bool rawLB = (state.Gamepad.wButtons & XINPUT_GAMEPAD_LEFT_THUMB) ? true : false;  |
| 98 | tPrintf("LB: %s\n" , rawLB ? "down" : "up" );  |
| 99 | // @todo Set LStick B.  |
| 100 |   |
| 101 | int16 rawRX = state.Gamepad.sThumbRX;  |
| 102 | float axisRXNorm = (rawRX < 0) ? float(rawRX)/32768.0f : float(rawRX)/32767.0f;  |
| 103 | // @todo Set RStick X.  |
| 104 |   |
| 105 | int16 rawRY = state.Gamepad.sThumbRY;  |
| 106 | float axisRYNorm = (rawRY < 0) ? float(rawRY)/32768.0f : float(rawRY)/32767.0f;  |
| 107 | // @todo Set RStick Y.  |
| 108 |   |
| 109 | bool rawRB = (state.Gamepad.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) ? true : false;  |
| 110 |   |
| 111 | float leftTriggerNorm = float(state.Gamepad.bLeftTrigger) / 255.0f;  |
| 112 | LTrigger.SetDisplacementRaw(leftTriggerNorm);  |
| 113 |   |
| 114 | float rightTriggerNorm = float(state.Gamepad.bRightTrigger) / 255.0f;  |
| 115 | RTrigger.SetDisplacementRaw(rightTriggerNorm);  |
| 116 |   |
| 117 | PollingPacketNumber = state.dwPacketNumber;  |
| 118 | }  |
| 119 | }  |
| 120 | else  |
| 121 | {  |
| 122 | // Controller is not connected. This can happen if the detection thread of the controller system has not  |
| 123 | // realized yet that the controller was disconnected (and stopped the polling thread). In this case we can  |
| 124 | // just stop polling. Note that exiting the loop does not stop the thread from being joinable. It is still  |
| 125 | // considered connected until the detection thread calls StopPolling. In the mean time, it only makes sense  |
| 126 | // to reset the components.  |
| 127 | LTrigger.Reset();  |
| 128 | RTrigger.Reset();  |
| 129 | break;  |
| 130 | }  |
| 131 | #endif  |
| 132 |   |
| 133 | // This unique_lock is just a more powerful version of lock_guard. Supports subsequent unlocking/locking which  |
| 134 | // is presumably needed by wait_for. In any case, wait_for needs this type of lock.  |
| 135 | std::unique_lock<std::mutex> lock(Mutex);  |
| 136 | bool exitRequested = PollingExitCondition.wait_for(lock&: lock, rtime: std::chrono::nanoseconds(PollingPeriod_ns), p: [this]{ return PollingExitRequested; });  |
| 137 | if (exitRequested)  |
| 138 | break;  |
| 139 | }  |
| 140 | }  |
| 141 |   |
| 142 |   |
| 143 | void tContGamepad::Update()  |
| 144 | {  |
| 145 | LStick.Update();  |
| 146 | RStick.Update();  |
| 147 | DPad.Update();  |
| 148 | LTrigger.Update();  |
| 149 | RTrigger.Update();  |
| 150 | LViewButton.Update();  |
| 151 | RMenuButton.Update();  |
| 152 | LBumperButton.Update();  |
| 153 | RBumperButton.Update();  |
| 154 | XButton.Update();  |
| 155 | YButton.Update();  |
| 156 | AButton.Update();  |
| 157 | BButton.Update();  |
| 158 | }  |
| 159 |   |
| 160 |   |
| 161 | void tContGamepad::SetDefinition()  |
| 162 | {  |
| 163 | #ifdef PLATFORM_WINDOWS  |
| 164 |   |
| 165 | tPrintf("Gamepad %d Set Definition" , int(GamepadID));  |
| 166 | WinXInputCapabilitiesEx capsEx;  |
| 167 | tStd::tMemclr(&capsEx, sizeof(WinXInputCapabilitiesEx));  |
| 168 | if (XInputGetCapabilitiesEx(1, int(GamepadID), 0, &capsEx) == WinErrorSuccess)  |
| 169 | {  |
| 170 | tVidPid vidpid(uint16(capsEx.vendorId), uint16(capsEx.productId));  |
| 171 | const tContDefn* defn = tLookupContDefn(vidpid);  |
| 172 | const char* vendor = defn ? defn->Vendor : "unknown" ;  |
| 173 | const char* product = defn ? defn->Product : "unknown" ;  |
| 174 | tPrintf  |
| 175 | (  |
| 176 | "Gamepad vid = 0x%04X pid = 0x%04X Vendor:%s Product:%s\n" ,  |
| 177 | int(capsEx.vendorId), int(capsEx.productId), vendor, product  |
| 178 | );  |
| 179 | if (defn)  |
| 180 | Definition = *defn;  |
| 181 | else  |
| 182 | Definition.SetGeneric();  |
| 183 | }  |
| 184 | else  |
| 185 | {  |
| 186 | tPrintf("Using generic definition\n" );  |
| 187 | Definition.SetGeneric();  |
| 188 | }  |
| 189 |   |
| 190 | #else  |
| 191 | Definition.SetGeneric();  |
| 192 | #endif  |
| 193 | }  |
| 194 |   |
| 195 |   |
| 196 | void tContGamepad::ClearDefinition()  |
| 197 | {  |
| 198 | Definition.Clear();  |
| 199 | }  |
| 200 |   |
| 201 |   |
| 202 | }  |
| 203 | |