| 1 | //************************************ bs::framework - Copyright 2018 Marko Pintera **************************************// |
| 2 | //*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********// |
| 3 | #include "Input/BsInput.h" |
| 4 | #include "Private/Linux/BsLinuxInput.h" |
| 5 | #include "Input/BsMouse.h" |
| 6 | #include "Input/BsKeyboard.h" |
| 7 | #include "Input/BsGamepad.h" |
| 8 | #include <fcntl.h> |
| 9 | #include <linux/input.h> |
| 10 | |
| 11 | namespace bs |
| 12 | { |
| 13 | /** Information about events reported from a specific input event device. */ |
| 14 | struct EventInfo |
| 15 | { |
| 16 | Vector<INT32> buttons; |
| 17 | Vector<INT32> relAxes; |
| 18 | Vector<INT32> absAxes; |
| 19 | Vector<INT32> hats; |
| 20 | }; |
| 21 | |
| 22 | /** Checks is the bit at the specified location in a byte array is set. */ |
| 23 | bool isBitSet(UINT8 bits[], UINT32 bit) |
| 24 | { |
| 25 | return ((bits[bit/8] >> (bit%8)) & 1) != 0; |
| 26 | } |
| 27 | |
| 28 | /** Returns information about an input event device attached to he provided file handle. */ |
| 29 | bool getEventInfo(int fileHandle, EventInfo& eventInfo) |
| 30 | { |
| 31 | UINT8 eventBits[1 + EV_MAX/8]; |
| 32 | bs_zero_out(eventBits); |
| 33 | |
| 34 | if (ioctl(fileHandle, EVIOCGBIT(0, sizeof(eventBits)), eventBits) == -1) |
| 35 | return false; |
| 36 | |
| 37 | for (UINT32 i = 0; i < EV_MAX; i++) |
| 38 | { |
| 39 | if(isBitSet(eventBits, i)) |
| 40 | { |
| 41 | if(i == EV_ABS) |
| 42 | { |
| 43 | UINT8 absAxisBits[1 + ABS_MAX/8]; |
| 44 | bs_zero_out(absAxisBits); |
| 45 | |
| 46 | if (ioctl(fileHandle, EVIOCGBIT(i, sizeof(absAxisBits)), absAxisBits) == -1) |
| 47 | { |
| 48 | LOGERR("Could not read device absolute axis features." ); |
| 49 | continue; |
| 50 | } |
| 51 | |
| 52 | for (UINT32 j = 0; j < ABS_MAX; j++) |
| 53 | { |
| 54 | if(isBitSet(absAxisBits, j)) |
| 55 | { |
| 56 | if(j >= ABS_HAT0X && j <= ABS_HAT3Y) |
| 57 | eventInfo.hats.push_back(j); |
| 58 | else |
| 59 | eventInfo.absAxes.push_back(j); |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | else if(i == EV_REL) |
| 64 | { |
| 65 | UINT8 relAxisBits[1 + REL_MAX/8]; |
| 66 | bs_zero_out(relAxisBits); |
| 67 | |
| 68 | if (ioctl(fileHandle, EVIOCGBIT(i, sizeof(relAxisBits)), relAxisBits) == -1) |
| 69 | { |
| 70 | LOGERR("Could not read device relative axis features." ); |
| 71 | continue; |
| 72 | } |
| 73 | |
| 74 | for (UINT32 j = 0; j < REL_MAX; j++) |
| 75 | { |
| 76 | if(isBitSet(relAxisBits, j)) |
| 77 | eventInfo.relAxes.push_back(j); |
| 78 | } |
| 79 | } |
| 80 | else if(i == EV_KEY) |
| 81 | { |
| 82 | UINT8 keyBits[1 + KEY_MAX/8]; |
| 83 | bs_zero_out(keyBits); |
| 84 | |
| 85 | if (ioctl(fileHandle, EVIOCGBIT(i, sizeof(keyBits)), keyBits) == -1) |
| 86 | { |
| 87 | LOGERR("Could not read device key features." ); |
| 88 | continue; |
| 89 | } |
| 90 | |
| 91 | for (UINT32 j = 0; j < KEY_MAX; j++) |
| 92 | { |
| 93 | if(isBitSet(keyBits, j)) |
| 94 | eventInfo.buttons.push_back(j); |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | } |
| 99 | |
| 100 | return true; |
| 101 | } |
| 102 | |
| 103 | /** Converts a Linux button code to Banshee ButtonCode. */ |
| 104 | ButtonCode gamepadMapCommonButton(INT32 code) |
| 105 | { |
| 106 | // Note: Assuming XBox controller layout here |
| 107 | switch (code) |
| 108 | { |
| 109 | case BTN_TRIGGER_HAPPY1: |
| 110 | return BC_GAMEPAD_DPAD_LEFT; |
| 111 | case BTN_TRIGGER_HAPPY2: |
| 112 | return BC_GAMEPAD_DPAD_RIGHT; |
| 113 | case BTN_TRIGGER_HAPPY3: |
| 114 | return BC_GAMEPAD_DPAD_UP; |
| 115 | case BTN_TRIGGER_HAPPY4: |
| 116 | return BC_GAMEPAD_DPAD_DOWN; |
| 117 | case BTN_START: |
| 118 | return BC_GAMEPAD_START; |
| 119 | case BTN_SELECT: |
| 120 | return BC_GAMEPAD_BACK; |
| 121 | case BTN_THUMBL: |
| 122 | return BC_GAMEPAD_LS; |
| 123 | case BTN_THUMBR: |
| 124 | return BC_GAMEPAD_RS; |
| 125 | case BTN_TL: |
| 126 | return BC_GAMEPAD_LB; |
| 127 | case BTN_TR: |
| 128 | return BC_GAMEPAD_RB; |
| 129 | case BTN_A: |
| 130 | return BC_GAMEPAD_A; |
| 131 | case BTN_B: |
| 132 | return BC_GAMEPAD_B; |
| 133 | case BTN_X: |
| 134 | return BC_GAMEPAD_X; |
| 135 | case BTN_Y: |
| 136 | return BC_GAMEPAD_Y; |
| 137 | } |
| 138 | |
| 139 | return BC_UNASSIGNED; |
| 140 | } |
| 141 | |
| 142 | /** |
| 143 | * Maps an absolute axis as reported by the Linux system, to a Banshee axis. This will be one of the InputAxis enum |
| 144 | * members, or -1 if it cannot be mapped. |
| 145 | */ |
| 146 | INT32 gamepadMapCommonAxis(INT32 axis) |
| 147 | { |
| 148 | switch(axis) |
| 149 | { |
| 150 | case ABS_X: return (INT32)InputAxis::LeftStickX; |
| 151 | case ABS_Y: return (INT32)InputAxis::LeftStickY; |
| 152 | case ABS_RX: return (INT32)InputAxis::RightStickX; |
| 153 | case ABS_RY: return (INT32)InputAxis::RightStickY; |
| 154 | case ABS_Z: return (INT32)InputAxis::LeftTrigger; |
| 155 | case ABS_RZ: return (INT32)InputAxis::RightTrigger; |
| 156 | } |
| 157 | |
| 158 | return -1; |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * Returns true if the input event attached to the specified file handle is a gamepad, |
| 163 | * and populates the gamepad info structure. Returns false otherwise. |
| 164 | */ |
| 165 | bool parseGamepadInfo(int fileHandle, int eventHandlerIdx, GamepadInfo& info) |
| 166 | { |
| 167 | EventInfo eventInfo; |
| 168 | if(!getEventInfo(fileHandle, eventInfo)) |
| 169 | return false; |
| 170 | |
| 171 | bool isGamepad = false; |
| 172 | |
| 173 | // Check for gamepad buttons |
| 174 | UINT32 unknownButtonIdx = 0; |
| 175 | for(auto& entry : eventInfo.buttons) |
| 176 | { |
| 177 | if((entry >= BTN_JOYSTICK && entry < BTN_GAMEPAD) |
| 178 | || (entry >= BTN_GAMEPAD && entry < BTN_DIGI) |
| 179 | || (entry >= BTN_WHEEL && entry < KEY_OK)) |
| 180 | { |
| 181 | ButtonCode bc = gamepadMapCommonButton(entry); |
| 182 | if(bc == BC_UNASSIGNED) |
| 183 | { |
| 184 | // Map to unnamed buttons |
| 185 | if(unknownButtonIdx < 20) |
| 186 | { |
| 187 | bc = (ButtonCode)((INT32)BC_GAMEPAD_BTN1 + unknownButtonIdx); |
| 188 | info.buttonMap[entry] = bc; |
| 189 | |
| 190 | unknownButtonIdx++; |
| 191 | } |
| 192 | } |
| 193 | else |
| 194 | info.buttonMap[entry] = bc; |
| 195 | |
| 196 | isGamepad = true; |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | if(isGamepad) |
| 201 | { |
| 202 | info.eventHandlerIdx = eventHandlerIdx; |
| 203 | |
| 204 | // Get device name |
| 205 | char name[128]; |
| 206 | if (ioctl(fileHandle, EVIOCGNAME(sizeof(name)), name) != -1) |
| 207 | info.name = String(name); |
| 208 | else |
| 209 | LOGERR("Could not read device name." ); |
| 210 | |
| 211 | // Get axis ranges |
| 212 | UINT32 unknownAxisIdx = 0; |
| 213 | for(auto& entry : eventInfo.absAxes) |
| 214 | { |
| 215 | AxisInfo& axisInfo = info.axisMap[entry]; |
| 216 | axisInfo.min = Gamepad::MIN_AXIS; |
| 217 | axisInfo.max = Gamepad::MAX_AXIS; |
| 218 | |
| 219 | input_absinfo absinfo; |
| 220 | if (ioctl(fileHandle, EVIOCGABS(entry), &absinfo) == -1) |
| 221 | { |
| 222 | LOGERR("Could not read absolute axis device features." ); |
| 223 | continue; |
| 224 | } |
| 225 | |
| 226 | axisInfo.min = absinfo.minimum; |
| 227 | axisInfo.max = absinfo.maximum; |
| 228 | |
| 229 | axisInfo.axisIdx = gamepadMapCommonAxis(entry); |
| 230 | if(axisInfo.axisIdx == -1) |
| 231 | { |
| 232 | axisInfo.axisIdx = (INT32)InputAxis::Count + unknownAxisIdx; |
| 233 | unknownAxisIdx++; |
| 234 | } |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | return isGamepad; |
| 239 | } |
| 240 | |
| 241 | void Input::initRawInput() |
| 242 | { |
| 243 | mPlatformData = bs_new<InputPrivateData>(); |
| 244 | |
| 245 | // Scan for valid gamepad devices |
| 246 | for(int i = 0; i < 64; ++i ) |
| 247 | { |
| 248 | String eventPath = "/dev/input/event" + toString(i); |
| 249 | int file = open(eventPath.c_str(), O_RDONLY |O_NONBLOCK); |
| 250 | if(file == -1) |
| 251 | { |
| 252 | // Note: We're ignoring failures due to permissions. The assumption is that gamepads won't have special |
| 253 | // permissions. If this assumption proves wrong, then using udev might be required to read gamepad input. |
| 254 | continue; |
| 255 | } |
| 256 | |
| 257 | GamepadInfo info; |
| 258 | if(parseGamepadInfo(file, i, info)) |
| 259 | { |
| 260 | info.id = (UINT32)mPlatformData->gamepadInfos.size(); |
| 261 | mPlatformData->gamepadInfos.push_back(info); |
| 262 | } |
| 263 | |
| 264 | close(file); |
| 265 | } |
| 266 | |
| 267 | mKeyboard = bs_new<Keyboard>("Keyboard" , this); |
| 268 | mMouse = bs_new<Mouse>("Mouse" , this); |
| 269 | |
| 270 | UINT32 numGamepads = getDeviceCount(InputDevice::Gamepad); |
| 271 | for (UINT32 i = 0; i < numGamepads; i++) |
| 272 | mGamepads.push_back(bs_new<Gamepad>(mPlatformData->gamepadInfos[i].name, mPlatformData->gamepadInfos[i], this)); |
| 273 | } |
| 274 | |
| 275 | void Input::cleanUpRawInput() |
| 276 | { |
| 277 | if (mMouse != nullptr) |
| 278 | bs_delete(mMouse); |
| 279 | |
| 280 | if (mKeyboard != nullptr) |
| 281 | bs_delete(mKeyboard); |
| 282 | |
| 283 | for (auto& gamepad : mGamepads) |
| 284 | bs_delete(gamepad); |
| 285 | |
| 286 | bs_delete(mPlatformData); |
| 287 | } |
| 288 | |
| 289 | UINT32 Input::getDeviceCount(InputDevice device) const |
| 290 | { |
| 291 | switch(device) |
| 292 | { |
| 293 | case InputDevice::Keyboard: return 1; |
| 294 | case InputDevice::Mouse: return 1; |
| 295 | case InputDevice::Gamepad: return (UINT32)mPlatformData->gamepadInfos.size(); |
| 296 | case InputDevice::Count: return 0; |
| 297 | } |
| 298 | |
| 299 | return 0; |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | |