Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/util/dinput_source.cpp
4214 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#define INITGUID
5
6
#include "dinput_source.h"
7
#include "input_manager.h"
8
#include "platform_misc.h"
9
10
#include "common/assert.h"
11
#include "common/bitutils.h"
12
#include "common/error.h"
13
#include "common/log.h"
14
#include "common/string_util.h"
15
16
#include "fmt/format.h"
17
18
#include <cmath>
19
#include <limits>
20
LOG_CHANNEL(DInputSource);
21
22
using PFNDIRECTINPUT8CREATE = HRESULT(WINAPI*)(HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID* ppvOut,
23
LPUNKNOWN punkOuter);
24
using PFNGETDFDIJOYSTICK = LPCDIDATAFORMAT(WINAPI*)();
25
26
DInputSource::DInputSource() = default;
27
28
DInputSource::~DInputSource()
29
{
30
m_controllers.clear();
31
m_dinput.Reset();
32
if (m_dinput_module)
33
FreeLibrary(m_dinput_module);
34
}
35
36
std::array<bool, DInputSource::NUM_HAT_DIRECTIONS> DInputSource::GetHatButtons(DWORD hat)
37
{
38
std::array<bool, NUM_HAT_DIRECTIONS> buttons = {};
39
40
const WORD hv = LOWORD(hat);
41
if (hv != 0xFFFF)
42
{
43
if ((hv >= 0 && hv < 9000) || hv >= 31500)
44
buttons[HAT_DIRECTION_UP] = true;
45
if (hv >= 4500 && hv < 18000)
46
buttons[HAT_DIRECTION_RIGHT] = true;
47
if (hv >= 13500 && hv < 27000)
48
buttons[HAT_DIRECTION_DOWN] = true;
49
if (hv >= 22500)
50
buttons[HAT_DIRECTION_LEFT] = true;
51
}
52
53
return buttons;
54
}
55
56
std::string DInputSource::GetDeviceIdentifier(u32 index)
57
{
58
return fmt::format("DInput-{}", index);
59
}
60
61
static constexpr std::array<const char*, DInputSource::NUM_HAT_DIRECTIONS> s_hat_directions = {
62
{"Up", "Down", "Left", "Right"}};
63
64
bool DInputSource::Initialize(const SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
65
{
66
m_dinput_module = LoadLibraryW(L"dinput8");
67
if (!m_dinput_module)
68
{
69
ERROR_LOG("Failed to load DInput module.");
70
return false;
71
}
72
73
PFNDIRECTINPUT8CREATE create = reinterpret_cast<PFNDIRECTINPUT8CREATE>(
74
reinterpret_cast<void*>(GetProcAddress(m_dinput_module, "DirectInput8Create")));
75
PFNGETDFDIJOYSTICK get_joystick_data_format =
76
reinterpret_cast<PFNGETDFDIJOYSTICK>(reinterpret_cast<void*>(GetProcAddress(m_dinput_module, "GetdfDIJoystick")));
77
if (!create || !get_joystick_data_format)
78
{
79
ERROR_LOG("Failed to get DInput function pointers.");
80
return false;
81
}
82
83
HRESULT hr = create(GetModuleHandleA(nullptr), DIRECTINPUT_VERSION, IID_IDirectInput8W,
84
reinterpret_cast<LPVOID*>(m_dinput.GetAddressOf()), nullptr);
85
m_joystick_data_format = get_joystick_data_format();
86
if (FAILED(hr) || !m_joystick_data_format)
87
{
88
ERROR_LOG("DirectInput8Create() failed: {}", static_cast<unsigned>(hr));
89
return false;
90
}
91
92
// need to release the lock while we're enumerating, because we call winId().
93
settings_lock.unlock();
94
const std::optional<WindowInfo> toplevel_wi(Host::GetTopLevelWindowInfo());
95
settings_lock.lock();
96
97
if (!toplevel_wi.has_value() || toplevel_wi->type != WindowInfo::Type::Win32)
98
{
99
ERROR_LOG("Missing top level window, cannot add DInput devices.");
100
return false;
101
}
102
103
m_toplevel_window = static_cast<HWND>(toplevel_wi->window_handle);
104
ReloadDevices();
105
return true;
106
}
107
108
void DInputSource::UpdateSettings(const SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock)
109
{
110
// noop
111
}
112
113
static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef)
114
{
115
static_cast<std::vector<DIDEVICEINSTANCEW>*>(pvRef)->push_back(*lpddi);
116
return DIENUM_CONTINUE;
117
}
118
119
bool DInputSource::ReloadDevices()
120
{
121
// detect any removals
122
PollEvents();
123
124
// look for new devices
125
std::vector<DIDEVICEINSTANCEW> devices;
126
m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY);
127
128
VERBOSE_LOG("Enumerated {} devices", devices.size());
129
130
bool changed = false;
131
for (DIDEVICEINSTANCEW inst : devices)
132
{
133
// do we already have this one?
134
if (std::any_of(m_controllers.begin(), m_controllers.end(),
135
[&inst](const ControllerData& cd) { return inst.guidInstance == cd.guid; }))
136
{
137
// yup, so skip it
138
continue;
139
}
140
141
ControllerData cd;
142
cd.guid = inst.guidInstance;
143
HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.GetAddressOf(), nullptr);
144
if (FAILED(hr)) [[unlikely]]
145
{
146
WARNING_LOG("Failed to create instance of device [{}, {}]",
147
StringUtil::WideStringToUTF8String(inst.tszProductName),
148
StringUtil::WideStringToUTF8String(inst.tszInstanceName));
149
continue;
150
}
151
152
const std::string name(StringUtil::WideStringToUTF8String(inst.tszProductName));
153
if (AddDevice(cd, name))
154
{
155
const u32 index = static_cast<u32>(m_controllers.size());
156
m_controllers.push_back(std::move(cd));
157
InputManager::OnInputDeviceConnected(MakeGenericControllerDeviceKey(InputSourceType::DInput, index),
158
GetDeviceIdentifier(index), name);
159
changed = true;
160
}
161
}
162
163
return changed;
164
}
165
166
void DInputSource::Shutdown()
167
{
168
while (!m_controllers.empty())
169
{
170
const u32 index = static_cast<u32>(m_controllers.size() - 1);
171
InputManager::OnInputDeviceDisconnected(MakeGenericControllerDeviceKey(InputSourceType::DInput, index),
172
GetDeviceIdentifier(static_cast<u32>(m_controllers.size() - 1)));
173
m_controllers.pop_back();
174
}
175
}
176
177
bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
178
{
179
HRESULT hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE);
180
if (FAILED(hr))
181
{
182
hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
183
if (FAILED(hr))
184
{
185
ERROR_LOG("Failed to set cooperative level for '{}'", name);
186
return false;
187
}
188
189
WARNING_LOG("Failed to set exclusive mode for '{}'", name);
190
}
191
192
hr = cd.device->SetDataFormat(m_joystick_data_format);
193
if (FAILED(hr))
194
{
195
ERROR_LOG("Failed to set data format for '{}'", name);
196
return false;
197
}
198
199
hr = cd.device->Acquire();
200
if (FAILED(hr))
201
{
202
ERROR_LOG("Failed to acquire device '{}'", name);
203
return false;
204
}
205
206
DIDEVCAPS caps = {};
207
caps.dwSize = sizeof(caps);
208
hr = cd.device->GetCapabilities(&caps);
209
if (FAILED(hr))
210
{
211
ERROR_LOG("Failed to get capabilities for '{}'", name);
212
return false;
213
}
214
215
cd.num_buttons = caps.dwButtons;
216
217
static constexpr const u32 axis_offsets[] = {OFFSETOF(DIJOYSTATE, lX), OFFSETOF(DIJOYSTATE, lY),
218
OFFSETOF(DIJOYSTATE, lZ), OFFSETOF(DIJOYSTATE, lRz),
219
OFFSETOF(DIJOYSTATE, lRx), OFFSETOF(DIJOYSTATE, lRy),
220
OFFSETOF(DIJOYSTATE, rglSlider[0]), OFFSETOF(DIJOYSTATE, rglSlider[1])};
221
for (const u32 offset : axis_offsets)
222
{
223
// ask for 16 bits of axis range
224
DIPROPRANGE range = {};
225
range.diph.dwSize = sizeof(range);
226
range.diph.dwHeaderSize = sizeof(range.diph);
227
range.diph.dwHow = DIPH_BYOFFSET;
228
range.diph.dwObj = static_cast<DWORD>(offset);
229
range.lMin = std::numeric_limits<s16>::min();
230
range.lMax = std::numeric_limits<s16>::max();
231
cd.device->SetProperty(DIPROP_RANGE, &range.diph);
232
233
// did it apply?
234
if (SUCCEEDED(cd.device->GetProperty(DIPROP_RANGE, &range.diph)))
235
cd.axis_offsets.push_back(offset);
236
}
237
238
cd.num_buttons = std::min(static_cast<u32>(caps.dwButtons), static_cast<u32>(std::size(cd.last_state.rgbButtons)));
239
cd.num_hats = std::min(static_cast<u32>(caps.dwPOVs), static_cast<u32>(std::size(cd.last_state.rgdwPOV)));
240
241
hr = cd.device->Poll();
242
if (hr == DI_NOEFFECT)
243
cd.needs_poll = false;
244
else if (hr != DI_OK)
245
WARNING_LOG("Polling device '{}' failed: {:08X}", name, static_cast<unsigned>(hr));
246
247
hr = cd.device->GetDeviceState(sizeof(cd.last_state), &cd.last_state);
248
if (hr != DI_OK)
249
WARNING_LOG("GetDeviceState() for '{}' failed: {:08X}", name, static_cast<unsigned>(hr));
250
251
INFO_LOG("{} has {} buttons, {} axes, {} hats", name, cd.num_buttons, static_cast<u32>(cd.axis_offsets.size()),
252
cd.num_hats);
253
254
return (cd.num_buttons > 0 || !cd.axis_offsets.empty() || cd.num_hats > 0);
255
}
256
257
void DInputSource::PollEvents()
258
{
259
for (size_t i = 0; i < m_controllers.size();)
260
{
261
ControllerData& cd = m_controllers[i];
262
if (cd.needs_poll)
263
cd.device->Poll();
264
265
DIJOYSTATE js;
266
HRESULT hr = cd.device->GetDeviceState(sizeof(js), &js);
267
if (hr == DIERR_INPUTLOST || hr == DIERR_NOTACQUIRED)
268
{
269
hr = cd.device->Acquire();
270
if (hr == DI_OK)
271
hr = cd.device->GetDeviceState(sizeof(js), &js);
272
273
if (hr != DI_OK)
274
{
275
InputManager::OnInputDeviceDisconnected(InputBindingKey{{.source_type = InputSourceType::DInput,
276
.source_index = static_cast<u32>(i),
277
.source_subtype = InputSubclass::None,
278
.modifier = InputModifier::None,
279
.invert = 0,
280
.data = 0}},
281
GetDeviceIdentifier(static_cast<u32>(i)));
282
m_controllers.erase(m_controllers.begin() + i);
283
continue;
284
}
285
}
286
else if (hr != DI_OK)
287
{
288
WARNING_LOG("GetDeviceState() failed: {:08X}", static_cast<unsigned>(hr));
289
i++;
290
continue;
291
}
292
293
CheckForStateChanges(i, js);
294
i++;
295
}
296
}
297
298
InputManager::DeviceList DInputSource::EnumerateDevices()
299
{
300
InputManager::DeviceList ret;
301
for (size_t i = 0; i < m_controllers.size(); i++)
302
{
303
DIDEVICEINSTANCEW dii;
304
dii.dwSize = sizeof(DIDEVICEINSTANCEW);
305
std::string name;
306
if (SUCCEEDED(m_controllers[i].device->GetDeviceInfo(&dii)))
307
name = StringUtil::WideStringToUTF8String(dii.tszProductName);
308
309
if (name.empty())
310
name = "Unknown";
311
312
ret.emplace_back(MakeGenericControllerDeviceKey(InputSourceType::DInput, static_cast<u32>(i)),
313
GetDeviceIdentifier(static_cast<u32>(i)), std::move(name));
314
}
315
316
return ret;
317
}
318
319
InputManager::VibrationMotorList DInputSource::EnumerateVibrationMotors(std::optional<InputBindingKey> for_device)
320
{
321
return {};
322
}
323
324
bool DInputSource::GetGenericBindingMapping(std::string_view device, GenericInputBindingMapping* mapping)
325
{
326
return {};
327
}
328
329
void DInputSource::UpdateMotorState(InputBindingKey key, float intensity)
330
{
331
// not supported
332
}
333
334
void DInputSource::UpdateMotorState(InputBindingKey large_key, InputBindingKey small_key, float large_intensity,
335
float small_intensity)
336
{
337
// not supported
338
}
339
340
bool DInputSource::ContainsDevice(std::string_view device) const
341
{
342
return device.starts_with("DInput-");
343
}
344
345
std::optional<InputBindingKey> DInputSource::ParseKeyString(std::string_view device, std::string_view binding)
346
{
347
if (!device.starts_with("DInput-") || binding.empty())
348
return std::nullopt;
349
350
const std::optional<s32> player_id = StringUtil::FromChars<s32>(device.substr(7));
351
if (!player_id.has_value() || player_id.value() < 0)
352
return std::nullopt;
353
354
InputBindingKey key = {};
355
key.source_type = InputSourceType::DInput;
356
key.source_index = static_cast<u32>(player_id.value());
357
358
if (binding.starts_with("+Axis") || binding.starts_with("-Axis"))
359
{
360
std::string_view end;
361
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(5), 10, &end);
362
if (!axis_index.has_value())
363
return std::nullopt;
364
365
key.source_subtype = InputSubclass::ControllerAxis;
366
key.data = axis_index.value();
367
key.modifier = (binding[0] == '-') ? InputModifier::Negate : InputModifier::None;
368
key.invert = (end == "~");
369
return key;
370
}
371
else if (binding.starts_with("FullAxis"))
372
{
373
std::string_view end;
374
const std::optional<u32> axis_index = StringUtil::FromChars<u32>(binding.substr(8), 10, &end);
375
if (!axis_index.has_value())
376
return std::nullopt;
377
378
key.source_subtype = InputSubclass::ControllerAxis;
379
key.data = axis_index.value();
380
key.modifier = InputModifier::FullAxis;
381
key.invert = (end == "~");
382
return key;
383
}
384
else if (binding.starts_with("Hat"))
385
{
386
if (binding[3] < '0' || binding[3] > '9' || binding.length() < 5)
387
return std::nullopt;
388
389
const u32 hat_index = binding[3] - '0';
390
const std::string_view hat_dir(binding.substr(4));
391
for (u32 i = 0; i < NUM_HAT_DIRECTIONS; i++)
392
{
393
if (hat_dir == s_hat_directions[i])
394
{
395
key.source_subtype = InputSubclass::ControllerButton;
396
key.data = MAX_NUM_BUTTONS + hat_index * NUM_HAT_DIRECTIONS + i;
397
return key;
398
}
399
}
400
401
// bad direction
402
return std::nullopt;
403
}
404
else if (binding.starts_with("Button"))
405
{
406
const std::optional<u32> button_index = StringUtil::FromChars<u32>(binding.substr(6));
407
if (!button_index.has_value())
408
return std::nullopt;
409
410
key.source_subtype = InputSubclass::ControllerButton;
411
key.data = button_index.value();
412
return key;
413
}
414
415
// unknown axis/button
416
return std::nullopt;
417
}
418
419
TinyString DInputSource::ConvertKeyToString(InputBindingKey key)
420
{
421
TinyString ret;
422
423
if (key.source_type == InputSourceType::DInput)
424
{
425
if (key.source_subtype == InputSubclass::ControllerAxis)
426
{
427
const char* modifier =
428
(key.modifier == InputModifier::FullAxis ? "Full" : (key.modifier == InputModifier::Negate ? "-" : "+"));
429
ret.format("DInput-{}/{}Axis{}{}", u32(key.source_index), modifier, u32(key.data), key.invert ? "~" : "");
430
}
431
else if (key.source_subtype == InputSubclass::ControllerButton && key.data >= MAX_NUM_BUTTONS)
432
{
433
const u32 hat_num = (key.data - MAX_NUM_BUTTONS) / NUM_HAT_DIRECTIONS;
434
const u32 hat_dir = (key.data - MAX_NUM_BUTTONS) % NUM_HAT_DIRECTIONS;
435
ret.format("DInput-{}/Hat{}{}", u32(key.source_index), hat_num, s_hat_directions[hat_dir]);
436
}
437
else if (key.source_subtype == InputSubclass::ControllerButton)
438
{
439
ret.format("DInput-{}/Button{}", u32(key.source_index), u32(key.data));
440
}
441
}
442
443
return ret;
444
}
445
446
TinyString DInputSource::ConvertKeyToIcon(InputBindingKey key, InputManager::BindingIconMappingFunction mapper)
447
{
448
return {};
449
}
450
451
std::unique_ptr<ForceFeedbackDevice> DInputSource::CreateForceFeedbackDevice(std::string_view device, Error* error)
452
{
453
Error::SetStringView(error, "Not supported on this input source.");
454
return {};
455
}
456
457
void DInputSource::CheckForStateChanges(size_t index, const DIJOYSTATE& new_state)
458
{
459
ControllerData& cd = m_controllers[index];
460
DIJOYSTATE& last_state = cd.last_state;
461
462
for (size_t i = 0; i < cd.axis_offsets.size(); i++)
463
{
464
LONG new_value;
465
LONG old_value;
466
std::memcpy(&old_value, reinterpret_cast<const u8*>(&cd.last_state) + cd.axis_offsets[i], sizeof(old_value));
467
std::memcpy(&new_value, reinterpret_cast<const u8*>(&new_state) + cd.axis_offsets[i], sizeof(new_value));
468
if (old_value != new_value)
469
{
470
std::memcpy(reinterpret_cast<u8*>(&cd.last_state) + cd.axis_offsets[i], &new_value, sizeof(new_value));
471
472
// TODO: Use the range from caps?
473
const float value = static_cast<float>(new_value) / (new_value < 0 ? 32768.0f : 32767.0f);
474
InputManager::InvokeEvents(
475
MakeGenericControllerAxisKey(InputSourceType::DInput, static_cast<u32>(index), static_cast<u32>(i)), value,
476
GenericInputBinding::Unknown);
477
}
478
}
479
480
for (u32 i = 0; i < cd.num_buttons; i++)
481
{
482
if (last_state.rgbButtons[i] != new_state.rgbButtons[i])
483
{
484
last_state.rgbButtons[i] = new_state.rgbButtons[i];
485
486
const float value = (new_state.rgbButtons[i] != 0) ? 1.0f : 0.0f;
487
InputManager::InvokeEvents(MakeGenericControllerButtonKey(InputSourceType::DInput, static_cast<u32>(index), i),
488
value, GenericInputBinding::Unknown);
489
}
490
}
491
492
for (u32 i = 0; i < cd.num_hats; i++)
493
{
494
if (last_state.rgdwPOV[i] != new_state.rgdwPOV[i])
495
{
496
// map hats to the last buttons
497
const std::array<bool, NUM_HAT_DIRECTIONS> old_buttons(GetHatButtons(last_state.rgdwPOV[i]));
498
const std::array<bool, NUM_HAT_DIRECTIONS> new_buttons(GetHatButtons(new_state.rgdwPOV[i]));
499
last_state.rgdwPOV[i] = new_state.rgdwPOV[i];
500
501
for (u32 j = 0; j < NUM_HAT_DIRECTIONS; j++)
502
{
503
if (old_buttons[j] != new_buttons[j])
504
{
505
const float value = (new_buttons[j] ? 1.0f : 0.0f);
506
InputManager::InvokeEvents(MakeGenericControllerButtonKey(InputSourceType::DInput, static_cast<u32>(index),
507
cd.num_buttons + (i * NUM_HAT_DIRECTIONS) + j),
508
value, GenericInputBinding::Unknown);
509
}
510
}
511
}
512
}
513
}
514
515
std::optional<float> DInputSource::GetCurrentValue(InputBindingKey key)
516
{
517
std::optional<float> ret;
518
519
if (key.source_type != InputSourceType::DInput)
520
return ret;
521
522
if (key.source_index >= m_controllers.size())
523
return ret;
524
525
const ControllerData& cd = m_controllers[key.source_index];
526
if (key.source_subtype == InputSubclass::ControllerAxis && key.data < cd.axis_offsets.size())
527
{
528
LONG value;
529
std::memcpy(&value, reinterpret_cast<const u8*>(&cd.last_state) + cd.axis_offsets[key.data], sizeof(value));
530
ret = static_cast<float>(value) / (value < 0 ? 32768.0f : 32767.0f);
531
}
532
else if (key.source_subtype == InputSubclass::ControllerButton)
533
{
534
if (key.data < cd.num_buttons)
535
{
536
ret = BoolToFloat(cd.last_state.rgbButtons[key.data]);
537
}
538
else
539
{
540
// might be a hat
541
const u32 hat_index = (key.data - cd.num_buttons) / NUM_HAT_DIRECTIONS;
542
const u32 hat_direction = (key.data - cd.num_buttons) % NUM_HAT_DIRECTIONS;
543
if (hat_index < cd.num_hats)
544
{
545
const std::array<bool, NUM_HAT_DIRECTIONS> buttons(GetHatButtons(cd.last_state.rgdwPOV[hat_index]));
546
ret = BoolToFloat(buttons[hat_direction]);
547
}
548
}
549
}
550
551
return ret;
552
}
553
554
std::unique_ptr<InputSource> InputSource::CreateDInputSource()
555
{
556
return std::make_unique<DInputSource>();
557
}
558
559