Files
spice2x.github.io/src/spice2x/rawinput/rawinput.cpp
T
bicarus 47d886306e rawinput: handle midi scanning asynchronously, fix midi hotplug (#793)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #603

## Description of change
Scanning for midi devices can take a while on Windows 11 with MIDI 2.0
service. Sometimes it'll take a couple seconds. Sometimes, on a PC with
zero MIDI devices, it takes 10-11 seconds.

This was causing two issues:

1. slow startup time
2. crash on invalid memory access due to a race condition

Address both.

Also fix a bug: when a device change event fires, we do a MIDI scan, and
invalidated all existing MIDI devices in favor of creating new handles.
Stop doing this, and instead check for duplicates by matching the ID and
keep existing device handles alone. Properly clean up devices on unplug.

Also: fix MIDI buttons being stuck on when unplugged while holding a
key.

## Testing
Tested with Nostroller in MIDI mode. rtpMIDI works too.
2026-07-11 21:57:55 -07:00

3262 lines
134 KiB
C++

#include "rawinput.h"
#include <cstdarg>
#include <utility>
#include <vector>
#include <objbase.h>
#include <setupapi.h>
#include "util/logging.h"
#include "external/robin_hood.h"
#include "util/precise_timer.h"
#include "util/time.h"
#include "util/utils.h"
#include "piuio.h"
#include "touch.h"
#include "acio/mdxf/mdxf_poll.h"
extern "C" {
#include "external/usbhidusage/usb-hid-usage.h"
}
namespace rawinput {
// settings
bool NOLEGACY = false;
uint8_t HID_LIGHT_BRIGHTNESS = 100; // 100%
bool ENABLE_SMX_STAGE = false;
bool ENABLE_SMX_DEDICAB = false;
int TOUCHSCREEN_RANGE_X = 0;
int TOUCHSCREEN_RANGE_Y = 0;
bool DUMP_HID_DEVICES_TO_LOG = false;
bool NAIVE_REQUIRE_FOCUS = true;
bool RAWINPUT_REQUIRE_FOCUS = false;
// set this to something slightly longer than 16.67ms (60Hz) so that I/O can pick it up
// this may need to be adjusted for each game in the future if there is a game that polls less
// often than 60Hz
uint32_t MIDI_NOTE_SUSTAIN = 20;
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
// the price we pay for making spice overlay consume from raw input
// making focus detection a nightmare
bool OS_WINDOW_ACTIVE = false;
}
namespace {
// when replacing a device slot in place, keep the old slot's per-device mutexes
// instead of the freshly allocated pair on `replacement`. their addresses stay
// stable, so a thread still holding a snapshot pointer to the slot (e.g. the
// output thread blocked on mutex_out, which is taken without devices_mutex)
// never locks freed memory. the freshly allocated pair is freed here rather
// than leaking the old one. the slot must already be destructed so both
// mutexes are unlocked
void reuse_device_mutexes(rawinput::Device &replacement, const rawinput::Device &existing) {
delete replacement.mutex;
delete replacement.mutex_out;
replacement.mutex = existing.mutex;
replacement.mutex_out = existing.mutex_out;
}
}
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
return rawinput::MIDI_NOTE_ALGORITHM;
}
void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
rawinput::MIDI_NOTE_ALGORITHM = new_algo;
std::string s = "Unknown";
switch (new_algo) {
case rawinput::MidiNoteAlgorithm::LEGACY:
s = "legacy";
break;
case rawinput::MidiNoteAlgorithm::V2:
s = "v2";
break;
case rawinput::MidiNoteAlgorithm::V2_DRUM:
s = "v2_drum";
break;
default:
log_info("rawinput", "assert failed: invalid midi algorithm");
break;
}
log_info("rawinput", "using MIDI algorithm: {}", s);
}
rawinput::RawInputManager::RawInputManager() {
XINPUT_MGR = std::make_unique<xinput::XInputManager>();
// create input window and load in devices
this->input_hwnd_create();
this->devices_reload();
// start flushing thread
this->output_start();
this->flush_start();
// now create the hotplug manager on that window
this->hotplug = new HotplugManager(this, this->input_hwnd);
}
rawinput::RawInputManager::~RawInputManager() {
this->stop();
log_info("rawinput", "destructor done");
}
void rawinput::RawInputManager::stop() {
if (this->hotplug) {
// remove hotplug
delete this->hotplug;
this->hotplug = nullptr;
}
// wait for any in-flight async MIDI scan before tearing down devices
this->midi_scan_join();
// unregister device messages
this->devices_unregister();
// stop threads
this->flush_stop();
this->output_stop();
// destruct all devices and input window
this->devices_destruct();
this->input_hwnd_destroy();
XINPUT_MGR.reset();
}
void rawinput::RawInputManager::input_hwnd_create() {
// register window class
this->input_hwnd_class.cbSize = sizeof(WNDCLASSEX);
this->input_hwnd_class.hInstance = GetModuleHandle(nullptr);
this->input_hwnd_class.lpfnWndProc = rawinput::RawInputManager::input_wnd_proc;
this->input_hwnd_class.lpszClassName = "SpiceTools Input";
if (!RegisterClassEx(&this->input_hwnd_class)) {
log_warning("rawinput", "could not register input class");
return;
}
// create input thread
this->input_thread = new std::thread([this]() {
// increase priority
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
// create window
this->input_hwnd = CreateWindowExA(
0,
this->input_hwnd_class.lpszClassName,
"SpiceTools Input",
0, 0, 0, 0, 0,
nullptr,
nullptr,
this->input_hwnd_class.hInstance,
this
);
// window loop
MSG msg;
while (GetMessage(&msg, this->input_hwnd, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
DestroyWindow(this->input_hwnd);
this->input_hwnd = nullptr;
});
// wait for window creation being done
timeutils::PreciseSleepTimer timer;
while (!this->input_hwnd) {
timer.sleep(1);
}
}
void rawinput::RawInputManager::input_hwnd_destroy() {
if (this->input_hwnd) {
// post close and join
PostMessage(this->input_hwnd, WM_CLOSE, 0, 0);
}
if (this->input_thread) {
this->input_thread->join();
// delete thread
delete this->input_thread;
this->input_thread = nullptr;
}
// unregister the window class
UnregisterClass(this->input_hwnd_class.lpszClassName, this->input_hwnd_class.hInstance);
}
void rawinput::RawInputManager::devices_reload() {
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
this->devices_destruct();
log_info("rawinput", "reloading devices...");
// scan for devices
this->devices_scan_rawinput();
// MIDI enumeration can block for ~10s while the Windows MIDI subsystem starts
// up, so run it off the init path instead of gating startup on it. it locks
// devices_mutex only for the list mutation, so it is safe to run concurrently
this->midi_scan_start();
this->devices_scan_piuio();
if (ENABLE_SMX_STAGE) {
this->devices_scan_smxstage();
}
if (ENABLE_SMX_DEDICAB) {
this->devices_scan_smxdedicab();
}
this->devices_scan_xinput();
// check for LIT Board
sextet_register("COM54", "LIT Board", false);
// register devices
this->devices_register();
}
void rawinput::RawInputManager::midi_scan_start() {
// single-flight: only one scan runs at a time. if one is already running, set
// the pending flag so it rescans once more when it finishes - MIDI hotplug
// events fire while the slow enumeration is still going and must not be lost.
// the scheduler mutex makes this check-and-set atomic with the worker's
// exit-or-rescan decision below, so a request set while a scan is running is
// never dropped
{
std::lock_guard<std::mutex> lock(this->midi_scan_m);
if (this->midi_scan_active) {
this->midi_scan_pending = true;
log_misc("rawinput", "MIDI scan already running, queued rescan");
return;
}
this->midi_scan_active = true;
this->midi_scan_pending = false;
}
// clean up the previous (already finished) scan thread handle
this->midi_scan_join();
// run the (potentially slow) MIDI enumeration on its own thread so callers are
// not blocked while the Windows MIDI subsystem starts up. rescan if a request
// arrived while we were scanning
log_misc("rawinput", "starting async MIDI scan thread");
this->midi_thread = new std::thread([this]() {
for (;;) {
this->devices_scan_midi();
// decide whether to exit under the scheduler lock, atomically with any
// concurrent midi_scan_start(): if a rescan was requested, consume it
// and loop; otherwise clear active and exit. because both sides take
// the same lock, a request set while active is true is never lost, so
// we never strand a hotplug event waiting for a future one
std::lock_guard<std::mutex> lock(this->midi_scan_m);
if (!this->midi_scan_pending) {
this->midi_scan_active = false;
log_misc("rawinput", "async MIDI scan thread finished");
return;
}
this->midi_scan_pending = false;
log_misc("rawinput", "async MIDI scan rescanning (event arrived during scan)");
}
});
}
void rawinput::RawInputManager::midi_scan_join() {
if (this->midi_thread) {
if (this->midi_thread->joinable()) {
// this blocks until the scan worker returns. if it ever hangs here the
// worker is stuck - most likely in midi_close_deferred_flush() waiting
// on a WinMM close. a missing "joined" line pinpoints the hang
log_misc("rawinput", "joining MIDI scan thread...");
this->midi_thread->join();
log_misc("rawinput", "MIDI scan thread joined");
}
delete this->midi_thread;
this->midi_thread = nullptr;
}
}
void rawinput::RawInputManager::midi_close_deferred_flush() {
// take the queued handles under the lock, then close them without it. WinMM
// midiInReset/midiInClose block until in-flight input_midi_proc callbacks
// return, and those callbacks take devices_mutex, so closing under the lock
// would deadlock
std::vector<HMIDIIN> handles;
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
handles.swap(this->midi_close_deferred);
}
if (handles.empty()) {
return;
}
// if a hang is ever reported here it is the classic WinMM deadlock: an
// in-flight input_midi_proc callback is blocked on devices_mutex while
// midiInReset/midiInClose waits for that callback to return. the per-handle
// log below pinpoints exactly which close did not come back
log_misc("rawinput", "closing {} deferred MIDI handle(s)", handles.size());
for (size_t i = 0; i < handles.size(); i++) {
log_misc("rawinput", "closing deferred MIDI handle {}/{}", i + 1, handles.size());
midiInReset(handles[i]);
midiInClose(handles[i]);
}
log_misc("rawinput", "deferred MIDI handles closed");
}
void rawinput::RawInputManager::devices_scan_rawinput(const std::string &device_name) {
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
log_misc("rawinput", "scan rawinput devices...");
// get number of devices
UINT device_no = 0;
if (GetRawInputDeviceList(nullptr, &device_no, sizeof(RAWINPUTDEVICELIST)) == (UINT)-1) {
return;
}
if (!device_no) {
return;
}
// get device list
std::shared_ptr<RAWINPUTDEVICELIST> device_list(new RAWINPUTDEVICELIST[device_no]);
GetRawInputDeviceList(device_list.get(), &device_no, sizeof(RAWINPUTDEVICELIST));
if (!device_no) {
return;
}
// iterate devices
for (UINT device_cur_index = 0; device_cur_index < device_no; device_cur_index++) {
auto device = &device_list.get()[device_cur_index];
if (device_name.length() == 0) {
devices_scan_rawinput(device, false);
} else if (device_name == rawinput::RawInputManager::rawinput_get_device_name(device->hDevice)) {
log_info("rawinput", "scanning device: {}", device_name);
devices_scan_rawinput(device, true);
}
}
}
void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log) {
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
// get device name
std::string device_name = rawinput_get_device_name(device->hDevice);
if (device_name.empty()) {
return;
}
log_misc("rawinput", "found rawinput device: {}", device_name);
// extract information out of name
auto device_info = rawinput::RawInputManager::get_device_info(device_name);
auto device_description = rawinput::RawInputManager::rawinput_get_device_description(device_info, device_name);
// get device information
RID_DEVICE_INFO rawinput_device_info {};
rawinput_device_info.cbSize = sizeof(RID_DEVICE_INFO);
UINT device_info_size = rawinput_device_info.cbSize;
if (GetRawInputDeviceInfo(device->hDevice, RIDI_DEVICEINFO, &rawinput_device_info, &device_info_size) == (UINT) -1) {
return;
}
// check for duplicate handle
size_t unique_id = devices.size() + 1;
size_t i = 0;
for (const auto &existing : this->devices) {
if (existing.name == device_name) {
unique_id = i;
break;
}
i++;
}
// build device
Device new_device {};
new_device.id = unique_id;
new_device.handle = device->hDevice;
new_device.name = device_name;
new_device.desc = device_description;
new_device.info = device_info;
new_device.mutex = new std::mutex();
new_device.mutex_out = new std::mutex();
new_device.input_time = get_performance_seconds();
switch (device->dwType) {
case RIM_TYPEMOUSE:
new_device.type = MOUSE;
new_device.mouseInfo = new DeviceMouseInfo();
break;
case RIM_TYPEKEYBOARD:
new_device.type = KEYBOARD;
new_device.keyboardInfo = new DeviceKeyboardInfo();
break;
case RIM_TYPEHID: {
new_device.type = HID;
HIDDriver hid_driver = HIDDriver::Default;
HIDD_ATTRIBUTES hid_attributes {};
// get preparsed information
UINT preparsed_size = 0;
if (GetRawInputDeviceInfo(
device->hDevice,
RIDI_PREPARSEDDATA,
nullptr,
&preparsed_size) == (UINT) -1)
{
return;
}
if (!preparsed_size) {
return;
}
// allocate buffer
auto preparsed_data = util::make_unique_plain<_HIDP_PREPARSED_DATA>(preparsed_size);
if (GetRawInputDeviceInfo(
device->hDevice,
RIDI_PREPARSEDDATA,
preparsed_data.get(),
&preparsed_size) == (UINT) -1)
{
return;
}
// get caps
_HIDP_CAPS caps {};
if (HidP_GetCaps(preparsed_data.get(), &caps) != HIDP_STATUS_SUCCESS) {
return;
}
// skip vendor-specific devices
//
// In the case of the Corsair Vengeance K70 RGB, the get device manufacturer and product
// string functions take 5 seconds each, which really delays the boot when it has three
// vendor-specific devices. Luckily, those three vendor-specific devices have the usage page
// appropriately set. This took Felix an hour to narrow down.
auto hid_vid = rawinput_device_info.hid.dwVendorId;
auto hid_pid = rawinput_device_info.hid.dwProductId;
if ((caps.UsagePage >> 8) == 0xFF
&& !(hid_vid == 0xBEEF && hid_pid == 0x5730)) // allow Minimaid
{
if (DUMP_HID_DEVICES_TO_LOG) {
log_misc("rawinput", "skipping vendor-specific device, vid/pid 0x{:04x}:0x{:04x}", hid_vid, hid_pid);
}
return;
}
// get usage description
std::string usage_name;
{
auto *usage_name_str = usb_hid_get_usage_text(caps.UsagePage, caps.Usage);
if (usage_name_str) {
usage_name = usage_name_str;
free(usage_name_str);
} else {
usage_name = fmt::format("Unknown (0x{:04x}:0x{:04x})", caps.UsagePage, caps.Usage);
}
}
// get better device description
HANDLE hid_handle = CreateFile(
device_name.c_str(), GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
0, nullptr);
if (hid_handle != INVALID_HANDLE_VALUE) {
// check manufacturer string
std::string man_ws;
wchar_t man_str_buffer[256] {};
if (HidD_GetManufacturerString(hid_handle, man_str_buffer, sizeof(man_str_buffer))) {
man_ws = wchar_to_u8(man_str_buffer);
}
// get product string
std::string prod_ws;
wchar_t prod_str_buffer[256] {};
if (HidD_GetProductString(hid_handle, prod_str_buffer, sizeof(prod_str_buffer))) {
prod_ws = wchar_to_u8(prod_str_buffer);
}
// For Bluetooth LE HID devices (e.g., newer Xbox controllers) HidD_GetProductString
// and HidD_GetManufacturerString will return blank strings.
// https://docs.microsoft.com/en-us/answers/questions/401236/hidd-getproductstring-with-ble-hid-device.html
if (man_ws.empty() && prod_ws.empty()) {
prod_ws = device_description;
}
// build desc string
std::string desc_ws;
if (string_begins_with(prod_ws, man_ws)) {
desc_ws = prod_ws;
} else {
desc_ws = man_ws;
if (!man_ws.empty() && !prod_ws.empty()) {
desc_ws += " ";
}
desc_ws += prod_ws;
}
new_device.desc = desc_ws;
// get attributes
if (!HidD_GetAttributes(hid_handle, &hid_attributes)) {
log_warning("rawinput", "failed to get HID device attributes for {}", device_name);
}
}
// get button caps
USHORT button_cap_length = caps.NumberInputButtonCaps;
std::vector<HIDP_BUTTON_CAPS> button_cap_data(static_cast<size_t>(button_cap_length));
if (button_cap_length > 0) {
if (HidP_GetButtonCaps(HidP_Input, button_cap_data.data(), &button_cap_length,
preparsed_data.get()) != HIDP_STATUS_SUCCESS)
{
return;
}
}
std::vector<HIDP_BUTTON_CAPS> button_caps_list;
std::vector<std::string> button_caps_names;
std::vector<std::vector<bool>> button_states;
std::vector<std::vector<double>> button_up, button_down;
std::map<std::pair<USAGE, ULONG>, ULONG> button_usage_pages;
for (int button_cap_num = 0; button_cap_num < button_cap_length; button_cap_num++) {
auto &button_caps = button_cap_data[button_cap_num];
// fill out range fields so we don't have to care later on
if (!button_caps.IsRange) {
button_caps.Range.UsageMin = button_caps.NotRange.Usage;
button_caps.Range.UsageMax = button_caps.NotRange.Usage;
button_caps.Range.DataIndexMin = button_caps.NotRange.DataIndex;
button_caps.Range.DataIndexMax = button_caps.NotRange.DataIndex;
button_caps.Range.DesignatorMin = button_caps.NotRange.DesignatorIndex;
button_caps.Range.DesignatorMax = button_caps.NotRange.DesignatorIndex;
button_caps.Range.StringMin = button_caps.NotRange.StringIndex;
button_caps.Range.StringMax = button_caps.NotRange.StringIndex;
}
int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1;
// ignore bad ranges reported by bad devices
if (button_count >= 0xffff) {
log_warning("rawinput", "skipping bad button cap range for device {}, range [{}, {}]",
device_name,
button_caps.Range.UsageMin,
button_caps.Range.UsageMax);
continue;
}
// fill vectors
button_caps_list.emplace_back(button_caps);
button_states.emplace_back(std::vector<bool>(static_cast<unsigned int>(button_count), false));
button_up.emplace_back(std::vector<double>(static_cast<unsigned int>(button_count), 0.0));
button_down.emplace_back(std::vector<double>(static_cast<unsigned int>(button_count), 0.0));
button_usage_pages[std::make_pair(button_caps.UsagePage, button_caps.LinkCollection)] += button_count;
// names
for (USAGE usg = button_caps.Range.UsageMin; usg <= button_caps.Range.UsageMax; usg++) {
const char *name = usb_hid_get_usage_text(button_caps.UsagePage, usg);
button_caps_names.emplace_back(name ? name : "Button Control");
free((void *) name);
}
}
// get button output caps
USHORT button_output_cap_length = caps.NumberOutputButtonCaps;
std::vector<HIDP_BUTTON_CAPS> button_output_cap_data(static_cast<size_t>(button_output_cap_length));
if (button_output_cap_length > 0) {
if (HidP_GetButtonCaps(HidP_Output, button_output_cap_data.data(), &button_output_cap_length,
preparsed_data.get()) != HIDP_STATUS_SUCCESS)
{
return;
}
}
std::vector<HIDP_BUTTON_CAPS> button_output_caps_list;
std::vector<std::string> button_output_caps_names;
std::vector<std::vector<bool>> button_output_states;
for (int button_cap_num = 0; button_cap_num < button_output_cap_length; button_cap_num++) {
auto &button_caps = button_output_cap_data[button_cap_num];
// fill out range fields so we don't have to care later on
if (!button_caps.IsRange) {
button_caps.Range.UsageMin = button_caps.NotRange.Usage;
button_caps.Range.UsageMax = button_caps.NotRange.Usage;
button_caps.Range.DataIndexMin = button_caps.NotRange.DataIndex;
button_caps.Range.DataIndexMax = button_caps.NotRange.DataIndex;
button_caps.Range.DesignatorMin = button_caps.NotRange.DesignatorIndex;
button_caps.Range.DesignatorMax = button_caps.NotRange.DesignatorIndex;
button_caps.Range.StringMin = button_caps.NotRange.StringIndex;
button_caps.Range.StringMax = button_caps.NotRange.StringIndex;
}
int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1;
// ignore bad ranges reported by bad devices
if (button_count >= 0xffff) {
log_warning("rawinput", "skipping bad button output cap range for device {}, range [{}, {}]",
device_name,
button_caps.Range.UsageMin,
button_caps.Range.UsageMax);
continue;
}
// fill vectors
button_output_caps_list.emplace_back(button_caps);
button_output_states.emplace_back(std::vector<bool>(button_count, false));
// names
for (USAGE usg = button_caps.Range.UsageMin; usg <= button_caps.Range.UsageMax; usg++) {
// check for custom name
wchar_t custom_name[256]{};
bool custom_name_set = false;
if (hid_handle != INVALID_HANDLE_VALUE) {
// get string index
ULONG string_index = 0;
if (button_caps.IsStringRange && button_caps.Range.StringMin != 0) {
string_index = button_caps.Range.StringMin + static_cast<ULONG>(button_output_caps_names.size());
}
else if (!button_caps.IsStringRange && button_caps.NotRange.StringIndex != 0) {
string_index = button_caps.NotRange.StringIndex;
}
// lookup string
if (string_index > 0 && HidD_GetIndexedString(
hid_handle,
string_index,
reinterpret_cast<void*>(custom_name),
sizeof(custom_name)))
{
custom_name_set = true;
}
}
// check if custom name is set
if (custom_name_set) {
// use custom name
button_output_caps_names.push_back(ws2s(std::wstring(custom_name)));
} else {
// lookup generic name
const char* name = usb_hid_get_usage_text(button_caps.UsagePage, usg);
button_output_caps_names.emplace_back(name ? name : "Button Control");
free((void*)name);
}
}
}
/*
* PacDrive LED driver board ("Ultimarc LED Controller")
* It's HID descriptor is trash so we need to fix that
*/
if (hid_attributes.VendorID == 0xD209 && (hid_attributes.ProductID & 0xFFF8) == 0x1500) {
hid_driver = HIDDriver::PacDrive;
// clear
button_output_caps_list.clear();
button_output_caps_names.clear();
button_output_states.clear();
// fake the output LEDs
for (int i = 0; i < 16; i++) {
// create generic indicator caps
HIDP_BUTTON_CAPS fakeCaps {};
fakeCaps.Range.UsageMin = 0x4B;
fakeCaps.Range.UsageMax = 0x4B;
// add content to lists
button_output_caps_list.push_back(fakeCaps);
button_output_caps_names.push_back("LED " + to_string(i + 1));
button_output_states.emplace_back(std::vector<bool>(1, false));
}
}
// get value caps
USHORT value_cap_length = caps.NumberInputValueCaps;
std::vector<HIDP_VALUE_CAPS> value_cap_data(value_cap_length);
if (value_cap_length > 0) {
if (HidP_GetValueCaps(HidP_Input, value_cap_data.data(), &value_cap_length,
preparsed_data.get()) != HIDP_STATUS_SUCCESS) {
return;
}
}
std::vector<HIDP_VALUE_CAPS> value_caps_list;
std::vector<std::string> value_caps_names;
std::vector<float> value_states(value_cap_length, 0.5f);
std::vector<LONG> value_states_raw(value_cap_length, 0);
std::vector<float> bind_value_states(value_cap_length, 0.5f);
// erratum for incorrect min/max reported by DJ DAO IIDX controller in HID-light mode
// (2012 version with updateable firmware)
bool is_dao_iidx =
(hid_attributes.VendorID == 0x1CCF &&
hid_attributes.ProductID == 0x8048 &&
new_device.desc == "MY-POWER CO.,LTD. PS3Controller");
for (int value_cap_num = 0; value_cap_num < value_cap_length; value_cap_num++) {
auto &value_caps = value_cap_data[value_cap_num];
if (is_dao_iidx && value_caps.BitSize == 8) {
log_info("rawinput", "Override analog range for device {}. Replacing [{}, {}] with [{}, {}]",
new_device.name,
value_caps.LogicalMin, value_caps.LogicalMax,
0, 255);
value_caps.LogicalMin = 0;
value_caps.LogicalMax = 255;
}
// fix up invalid max values (seen on xbox controllers where max is 0xffffffff despite being 16-bit)
if (value_caps.LogicalMin == 0 && value_caps.BitSize > 0 && value_caps.BitSize < 32) {
const uint32_t field_max = (1u << value_caps.BitSize) - 1u;
const uint32_t logical_max = static_cast<uint32_t>(value_caps.LogicalMax);
if (logical_max > field_max) {
log_info(
"rawinput",
"value cap {} LogicalMax exceeds bit width, fixing it up: {} -> {}",
value_cap_num,
value_caps.LogicalMax,
field_max
);
value_caps.LogicalMax = static_cast<LONG>(field_max);
}
}
// fix up hat switch to initially report as neutral position
if (value_caps.UsagePage == 0x1 && value_caps.Range.UsageMin == 0x39) {
value_states[value_cap_num] = -1.f;
}
// add to list
value_caps_list.emplace_back(value_caps);
// names
const char *name = usb_hid_get_usage_text(value_caps.UsagePage, value_caps.Range.UsageMin);
value_caps_names.emplace_back(name ? name : "Analog Control");
free((void *) name);
}
// get value output caps
USHORT value_output_cap_length = caps.NumberOutputValueCaps;
std::vector<HIDP_VALUE_CAPS> value_output_cap_data(static_cast<size_t>(value_output_cap_length));
if (value_output_cap_length > 0) {
if (HidP_GetValueCaps(HidP_Output, value_output_cap_data.data(), &value_output_cap_length,
preparsed_data.get()) != HIDP_STATUS_SUCCESS) {
return;
}
}
std::vector<HIDP_VALUE_CAPS> value_output_caps_list;
std::vector<std::string> value_output_caps_names;
std::vector<float> value_output_states;
for (size_t value_cap_num = 0; value_cap_num < value_output_cap_length; value_cap_num++) {
auto &value_caps = value_output_cap_data[value_cap_num];
// fix min and max values
if (value_caps.BitSize > 0 && value_caps.BitSize <= sizeof(value_caps.LogicalMin) * 8) {
auto shift_size = sizeof(value_caps.LogicalMin) * 8 - value_caps.BitSize + 1;
auto mask = ((uint64_t) 1 << value_caps.BitSize) - 1;
value_caps.LogicalMin &= mask;
value_caps.LogicalMin <<= shift_size;
value_caps.LogicalMin >>= shift_size;
value_caps.LogicalMax &= mask;
}
// check if this is a range cap
if (value_caps.IsRange) {
// add a cap for each value for range caps
auto usage_min = value_caps.Range.UsageMin;
auto usage_max = value_caps.Range.UsageMax;
for (auto usage = usage_min; usage <= usage_max; usage++) {
// add to list
value_caps.NotRange.Usage = usage;
value_output_caps_list.push_back(value_caps);
value_output_states.push_back(0.f);
// check for custom name
wchar_t custom_name[256] {};
bool custom_name_set = false;
if (hid_handle != INVALID_HANDLE_VALUE) {
// get string index
ULONG string_index = 0;
if (value_caps.IsStringRange && value_caps.Range.StringMin != 0) {
string_index = value_caps.Range.StringMin
+ static_cast<ULONG>(value_output_caps_list.size()) - 1;
} else if (!value_caps.IsStringRange && value_caps.NotRange.StringIndex != 0) {
string_index = value_caps.NotRange.StringIndex;
}
// lookup string
if (string_index > 0 && HidD_GetIndexedString(
hid_handle,
string_index,
reinterpret_cast<void *>(custom_name),
sizeof(custom_name))) {
custom_name_set = true;
}
}
// check if custom name is set
if (custom_name_set) {
// use custom name
value_output_caps_names.push_back(ws2s(std::wstring(custom_name)));
} else {
// lookup generic name
const char *name = usb_hid_get_usage_text(value_caps.UsagePage, usage);
value_output_caps_names.emplace_back(name ? name : "Value Output");
free((void *) name);
}
}
} else {
// add to list
value_output_caps_list.emplace_back(value_caps);
value_output_states.push_back(0.f);
// check for custom name
wchar_t custom_name[256] {};
bool custom_name_set = false;
if (hid_handle != INVALID_HANDLE_VALUE) {
// get string index
ULONG string_index = 0;
if (!value_caps.IsStringRange && value_caps.NotRange.StringIndex != 0) {
string_index = value_caps.NotRange.StringIndex;
}
// lookup string
if (string_index > 0 && HidD_GetIndexedString(
hid_handle,
string_index,
reinterpret_cast<void *>(custom_name),
sizeof(custom_name))) {
custom_name_set = true;
}
}
// check if custom name is set
if (custom_name_set) {
// use custom name
value_output_caps_names.push_back(ws2s(std::wstring(custom_name)));
} else {
// lookup generic name
const char *name = usb_hid_get_usage_text(value_caps.UsagePage, value_caps.NotRange.Usage);
value_output_caps_names.emplace_back(name ? name : "Value Output");
free((void *) name);
}
}
}
// generate HID info
new_device.hidInfo = new DeviceHIDInfo();
new_device.hidInfo->handle = hid_handle;
new_device.hidInfo->caps = caps;
new_device.hidInfo->attributes = hid_attributes;
new_device.hidInfo->driver = hid_driver;
new_device.hidInfo->usage_name = std::move(usage_name);
new_device.hidInfo->preparsed_data = std::move(preparsed_data);
new_device.hidInfo->preparsed_size = preparsed_size;
new_device.hidInfo->button_caps_list = std::move(button_caps_list);
new_device.hidInfo->button_caps_names = std::move(button_caps_names);
new_device.hidInfo->button_output_caps_list = std::move(button_output_caps_list);
new_device.hidInfo->button_output_caps_names = std::move(button_output_caps_names);
new_device.hidInfo->value_caps_list = std::move(value_caps_list);
new_device.hidInfo->value_caps_names = std::move(value_caps_names);
new_device.hidInfo->value_output_caps_list = std::move(value_output_caps_list);
new_device.hidInfo->value_output_caps_names = std::move(value_output_caps_names);
new_device.hidInfo->button_states = std::move(button_states);
new_device.hidInfo->button_up = std::move(button_up);
new_device.hidInfo->button_down = std::move(button_down);
new_device.hidInfo->button_output_states = std::move(button_output_states);
new_device.hidInfo->button_usage_pages = std::move(button_usage_pages);
new_device.hidInfo->value_states = std::move(value_states);
new_device.hidInfo->value_states_raw = std::move(value_states_raw);
new_device.hidInfo->value_output_states = std::move(value_output_states);
new_device.hidInfo->bind_value_states = std::move(bind_value_states);
// check for touch screen
if (rawinput::touch::is_touchscreen(&new_device)) {
rawinput::touch::enable(&new_device);
}
break;
}
default:
return;
}
// overwrite device with the same handle
for (auto &prev_device : this->devices) {
if (prev_device.name == new_device.name) {
log_info("rawinput", "overwriting existing device: {} / {}", new_device.desc, new_device.name);
// carry over old device ID
new_device.id = prev_device.id;
// destruct and replace, reusing the slot's existing mutexes
this->devices_destruct(&prev_device);
reuse_device_mutexes(new_device, prev_device);
prev_device = new_device;
// notify change
for (auto &cb : this->callback_change) {
cb.f(cb.data, &prev_device);
}
return;
}
}
// add device to list
auto &added_device = this->devices.emplace_back(new_device);
if (log) {
log_info("rawinput", "added device: {} / {}", added_device.desc, added_device.name);
}
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &added_device);
}
}
void rawinput::RawInputManager::devices_scan_midi() {
log_misc("rawinput", "scan MIDI devices...");
// note: the WinMM MIDI calls below (midiInGetNumDevs / midiInGetDevCaps /
// midiInOpen / midiInStart) can block for seconds while the Windows MIDI
// subsystem starts up, so they must NOT run under devices_mutex. only the
// list mutation at the end of each iteration is guarded.
// identifiers of every MIDI device seen in this scan; used below to
// tombstone devices that have since been unplugged
std::vector<std::string> present_identifiers;
// add midi devices
auto midi_device_count = midiInGetNumDevs();
for (size_t midi_device_id = 0; midi_device_id < midi_device_count; midi_device_id++) {
// get dev caps
MIDIINCAPS midi_device_caps{};
if (midiInGetDevCaps(midi_device_id, &midi_device_caps, sizeof(MIDIINCAPS)) != MMSYSERR_NOERROR) {
continue;
}
log_misc("rawinput", "found MIDI device: id {}, name {}, mid {}, pid {}",
midi_device_id, midi_device_caps.szPname, midi_device_caps.wMid, midi_device_caps.wPid);
// build identifier for MIDI
// ;MIDI; format is now set in stone (in other parts of the code base and in the config xml file)
// so it should never be changed
std::ostringstream midi_identifier_stream;
midi_identifier_stream << ";" << "MIDI";
midi_identifier_stream << ";" << midi_device_id;
midi_identifier_stream << ";" << midi_device_caps.szPname;
midi_identifier_stream << ";" << midi_device_caps.wMid;
midi_identifier_stream << ";" << midi_device_caps.wPid;
const auto midi_identifier = midi_identifier_stream.str();
// record that this device is currently present
present_identifiers.push_back(midi_identifier);
// if already open, leave it alone: hotplug fires many change events, and
// reopening on every rescan would drop the WinMM handle (and its input).
// only (re)open when the device is missing or a destroyed tombstone
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
bool already_open = false;
for (auto &device : this->devices) {
if (device.type == MIDI && device.name == midi_identifier) {
already_open = true;
break;
}
}
if (already_open) {
continue;
}
}
// open device
HMIDIIN midi_device_handle;
if (midiInOpen(&midi_device_handle,
(UINT) midi_device_id,
(DWORD_PTR) &input_midi_proc,
(DWORD_PTR) this,
CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
{
continue;
}
// start input
if (midiInStart(midi_device_handle) != MMSYSERR_NOERROR) {
// close the handle we just opened so it does not leak on repeated rescans
midiInClose(midi_device_handle);
continue;
}
// device info
DeviceInfo midi_device_info {};
// device midi info
auto midi_device_midi_info = new DeviceMIDIInfo();
midi_device_midi_info->states = std::vector<bool>(16 * 128);
midi_device_midi_info->states_events = std::vector<uint8_t>(16 * 128);
midi_device_midi_info->bind_states = std::vector<bool>(16 * 128);
midi_device_midi_info->v2_last_on_time = std::vector<double>(16 * 128);
midi_device_midi_info->v2_last_off_time = std::vector<double>(16 * 128);
midi_device_midi_info->v2_velocity_threshold = std::vector<uint8_t>(16 * 128);
midi_device_midi_info->v2_velocity_threshold_set_on_device = std::vector<bool>(16 * 128);
midi_device_midi_info->velocity = std::vector<uint8_t>(16 * 128);
midi_device_midi_info->freeze = false;
midi_device_midi_info->controls_precision = std::vector<uint16_t>(16 * 32);
midi_device_midi_info->controls_precision_bind = std::vector<uint16_t>(16 * 32);
midi_device_midi_info->controls_precision_msb = std::vector<bool>(16 * 32);
midi_device_midi_info->controls_precision_lsb = std::vector<bool>(16 * 32);
midi_device_midi_info->controls_precision_set = std::vector<bool>(16 * 32);
midi_device_midi_info->controls_single = std::vector<uint8_t>(16 * 44);
midi_device_midi_info->controls_single_bind = std::vector<uint8_t>(16 * 44);
midi_device_midi_info->controls_single_set = std::vector<bool>(16 * 44);
midi_device_midi_info->controls_onoff = std::vector<bool>(16 * 6);
midi_device_midi_info->controls_onoff_bind = std::vector<bool>(16 * 6);
midi_device_midi_info->controls_onoff_set = std::vector<bool>(16 * 6);
midi_device_midi_info->v2_controls_onoff_last_on_time = std::vector<double>(16 * 6);
midi_device_midi_info->v2_controls_onoff_last_off_time = std::vector<double>(16 * 6);
midi_device_midi_info->pitch_bend = std::vector<int16_t>(16 * 6);
midi_device_midi_info->pitch_bend_set = std::vector<bool>(16 * 6);
// build device
Device midi_device {};
midi_device.type = MIDI;
midi_device.handle = midi_device_handle;
midi_device.name = midi_identifier;
midi_device.desc = to_string(midi_device_caps.szPname);
midi_device.info = midi_device_info;
midi_device.mutex = new std::mutex();
midi_device.mutex_out = new std::mutex();
midi_device.midiInfo = midi_device_midi_info;
// mutate the shared device list under lock (the slow WinMM calls above
// ran without it so other threads were not blocked)
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
midi_device.id = devices.size() + 1;
// reuse a previously destroyed tombstone with the same identifier, if any.
// (a live device with this identifier was already skipped above)
bool replaced = false;
for (auto &device : this->devices) {
if (device.name == midi_identifier) {
// carry over ID
midi_device.id = device.id;
// destruct and replace, reusing the slot's existing mutexes
this->devices_destruct(&device);
reuse_device_mutexes(midi_device, device);
device = midi_device;
// notify change
for (auto &cb : this->callback_change) {
cb.f(cb.data, &device);
}
replaced = true;
break;
}
}
if (replaced) {
continue;
}
// add device to list
auto &device = this->devices.emplace_back(midi_device);
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &device);
}
}
// tombstone MIDI devices that were open but are no longer present (unplugged).
// otherwise a replugged device matches the stale live entry in the skip check
// above and never gets reopened, silently losing its input
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
for (auto &device : this->devices) {
if (device.type != MIDI) {
continue;
}
bool present = false;
for (const auto &identifier : present_identifiers) {
if (identifier == device.name) {
present = true;
break;
}
}
if (!present) {
log_info("rawinput", "MIDI device unplugged, releasing: {}", device.desc);
this->devices_destruct(&device);
}
}
}
// close the MIDI handles detached above, now that devices_mutex is released
this->midi_close_deferred_flush();
log_misc("rawinput", "scan MIDI devices done ({} enumerated)", (unsigned) midi_device_count);
}
void rawinput::RawInputManager::devices_scan_piuio() {
log_misc("rawinput", "scan PIUIO devices...");
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
// add device to vector first so pointer is valid
auto *new_piuio_device = new Device();
new_piuio_device->id = this->devices.size() + 1;
new_piuio_device->type = PIUIO_DEVICE;
new_piuio_device->name = "piuio";
new_piuio_device->desc = "PIUIO";
new_piuio_device->piuioDev = nullptr;
new_piuio_device->mutex = new std::mutex();
new_piuio_device->mutex_out = new std::mutex();
// try to initialize
auto &device = this->devices.emplace_back(*new_piuio_device);
auto piuioDev = new PIUIO(&device);
if (piuioDev->Init()) {
// successful initialization
device.piuioDev = piuioDev;
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &device);
}
} else {
// remove device since connection failed
this->devices.pop_back();
}
}
void rawinput::RawInputManager::devices_scan_smxstage() {
log_misc("rawinput", "scan SMX Stage devices...");
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
auto *new_smxstage_device = new Device();
new_smxstage_device->id = this->devices.size() + 1;
new_smxstage_device->type = SMX_STAGE;
new_smxstage_device->name = "smxstage";
new_smxstage_device->desc = "SMX Stage";
new_smxstage_device->smxstageInfo = nullptr;
new_smxstage_device->mutex = new std::mutex();
new_smxstage_device->mutex_out = new std::mutex();
auto &device = this->devices.emplace_back(*new_smxstage_device);
auto smxstageInfo = new SmxStageDevice();
if (smxstageInfo->Initialize()) {
device.smxstageInfo = smxstageInfo;
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &device);
}
} else {
// remove device since connection failed
this->devices.pop_back();
}
}
void rawinput::RawInputManager::devices_scan_smxdedicab() {
log_misc("rawinput", "scan SMX Dedicated Cabinet devices...");
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
auto *new_smxdedicab_device = new Device();
new_smxdedicab_device->id = this->devices.size() + 1;
new_smxdedicab_device->type = SMX_DEDICAB;
new_smxdedicab_device->name = "smxdedicab";
new_smxdedicab_device->desc = "SMX Dedicated Cabinet";
new_smxdedicab_device->smxdedicabInfo = nullptr;
new_smxdedicab_device->mutex = new std::mutex();
new_smxdedicab_device->mutex_out = new std::mutex();
auto &device = this->devices.emplace_back(*new_smxdedicab_device);
auto smxdedicabInfo = new SmxDedicabDevice();
if (smxdedicabInfo->Initialize()) {
device.smxdedicabInfo = smxdedicabInfo;
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &device);
}
} else {
// remove device since connection failed
this->devices.pop_back();
}
}
void rawinput::RawInputManager::devices_scan_xinput() {
log_misc("rawinput", "scan XInput devices...");
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
const auto connected_players = XINPUT_MGR->get_available_players();
// first, destroy missing devices
std::vector<std::string> devices_to_remove;
for (auto &device : this->devices) {
if (device.type != XINPUT_GAMEPAD) {
continue;
}
const uint8_t player = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(device.handle));
if (std::find(connected_players.begin(), connected_players.end(), player) == connected_players.end()) {
devices_to_remove.push_back(device.name);
}
}
for (const auto &name : devices_to_remove) {
this->devices_remove(name);
}
auto create_device = [](const uint8_t player) -> Device {
Device device = {};
device.type = XINPUT_GAMEPAD;
device.name = xinput::get_device_desc(player);
device.desc = fmt::format("XInput Gamepad P{}", player + 1);
device.handle = reinterpret_cast<HANDLE>(player);
device.mutex = new std::mutex();
device.mutex_out = new std::mutex();
return device;
};
// add new devices
for (const auto player : connected_players) {
bool duplicate_found = false;
// check for duplicates first
for (auto &prev_device : this->devices) {
if (prev_device.name != xinput::get_device_desc(player)) {
continue;
}
if (prev_device.type == DESTROYED) {
log_info("rawinput", "overwriting previously destroyed XInput device: {}", prev_device.name);
const auto old_id = prev_device.id;
// replace in place, reusing the slot's existing mutexes
auto replacement = create_device(player);
reuse_device_mutexes(replacement, prev_device);
prev_device = replacement;
prev_device.id = old_id;
// notify change
for (auto &cb : this->callback_change) {
cb.f(cb.data, &prev_device);
}
}
duplicate_found = true;
break;
}
if (!duplicate_found) {
// add new device
log_info("rawinput", "adding new XInput device: player {}", player + 1);
auto new_xinput_device = create_device(player);
new_xinput_device.id = this->devices.size() + 1;
auto &device = this->devices.emplace_back(new_xinput_device);
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &device);
}
}
}
}
void rawinput::RawInputManager::flush_start() {
// start flush thread
if (this->flush_thread == nullptr) {
this->flush_thread_running = true;
this->flush_thread = new std::thread([this] {
while (this->flush_thread_running) {
/*
* Write output report all ~500ms so DAO IIDX boards (and probably more) don't go back
* to button based lighting.
*/
this->devices_flush_output(false);
// wait up to ~500ms, but wake immediately if flush_stop()
// flips the running flag. Without the CV the in-flight
// Sleep() forced launcher::shutdown() to block for the full
// remaining sleep window on every close.
std::unique_lock<std::mutex> lock(this->flush_thread_m);
this->flush_thread_cv.wait_for(
lock,
std::chrono::milliseconds(495),
[this] { return !this->flush_thread_running; });
}
});
}
}
void rawinput::RawInputManager::flush_stop() {
// set stop flag and wake the flush thread immediately so shutdown
// isn't blocked by the in-progress wait inside the loop above.
{
std::lock_guard<std::mutex> lock(this->flush_thread_m);
this->flush_thread_running = false;
}
this->flush_thread_cv.notify_all();
// check if thread is set
if (this->flush_thread) {
// join and kill
this->flush_thread->join();
delete this->flush_thread;
// unset thread
this->flush_thread = nullptr;
}
}
void rawinput::RawInputManager::output_start() {
// start thread if required
if (!output_thread) {
output_thread_running = true;
output_thread = new std::thread([this] {
std::unique_lock<std::mutex> lock(output_thread_m);
while (output_thread_running) {
// wait for CV
output_thread_cv.wait(lock, [this] {
return output_thread_ready;
});
// check for exit
if (!output_thread_running) {
break;
}
// iterate all devices
do {
output_thread_ready = false;
this->devices_write_output_snapshot(true);
} while (output_thread_ready);
}
});
}
}
void rawinput::RawInputManager::output_stop() {
// stop output thread
this->output_thread_running = false;
if (this->output_thread) {
this->output_thread_m.lock();
this->output_thread_ready = true;
this->output_thread_m.unlock();
this->output_thread_cv.notify_all();
this->output_thread->join();
delete this->output_thread;
this->output_thread = nullptr;
}
}
std::string rawinput::RawInputManager::rawinput_get_device_name(HANDLE hDevice) {
// get device name length
UINT device_name_len = 0;
if (GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, nullptr, &device_name_len) == (UINT) -1) {
return "";
}
// allocate buffer
auto device_name = std::make_unique<char[]>(device_name_len);
// get device name (but it is actually the path)
if (GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, device_name.get(), &device_name_len) == (UINT) -1) {
return "";
}
if (device_name_len < 4) {
return "";
}
// the infamous XP fix
// see http://stackoverflow.com/questions/10798798
device_name[1] = '\\'; //
// build string
return std::string(device_name.get());
}
std::string rawinput::RawInputManager::rawinput_get_device_description(const rawinput::DeviceInfo &info,
const std::string &device_name) {
// yes this whole motherfucker is just for the device name - gotta <3 microsoft
std::string device_description;
HDEVINFO devinfo = SetupDiGetClassDevs(&info.guid, nullptr, nullptr, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
SP_DEVINFO_DATA devinfo_data{};
devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA);
for (DWORD i1 = 0; SetupDiEnumDeviceInfo(devinfo, i1, &devinfo_data); i1++) {
SP_DEVICE_INTERFACE_DATA i_data{};
i_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
for (DWORD i2 = 0; SetupDiEnumDeviceInterfaces(devinfo, &devinfo_data, &info.guid, i2, &i_data); i2++) {
// get device path
DWORD detail_data_size = 0;
if (SetupDiGetDeviceInterfaceDetail(devinfo, &i_data, nullptr, 0, &detail_data_size, nullptr)) {
continue;
}
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
continue;
}
// allocate buffer
std::unique_ptr<SP_DEVICE_INTERFACE_DETAIL_DATA> detail_data(
reinterpret_cast<SP_DEVICE_INTERFACE_DETAIL_DATA *>(new uint8_t[detail_data_size])
);
detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
if (!SetupDiGetDeviceInterfaceDetail(devinfo, &i_data, detail_data.get(), detail_data_size,
nullptr, nullptr))
{
continue;
}
std::string device_path(detail_data->DevicePath);
// the XP fix again
if (device_path.length() > 1) {
device_path[1] = '\\';
}
// check if this is our device (must be case insensitive)
if (_stricmp(device_path.c_str(), device_name.c_str()) == 0) {
// get property
DWORD desc_size = 0;
if (SetupDiGetDeviceRegistryPropertyW(
devinfo, &devinfo_data, SPDRP_DEVICEDESC, nullptr, nullptr, 0, &desc_size))
{
continue;
}
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
continue;
}
// allocate buffer
auto desc_data = std::make_unique<BYTE[]>(desc_size);
if (!SetupDiGetDeviceRegistryPropertyW(
devinfo, &devinfo_data, SPDRP_DEVICEDESC, nullptr, desc_data.get(), desc_size, nullptr))
{
continue;
}
// base description
device_description = wchar_to_u8(reinterpret_cast<PWCHAR>(desc_data.get()));
// append HID product string if available
HANDLE hid_handle = CreateFile(
device_path.c_str(), 0,
FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr,
OPEN_EXISTING,
0, nullptr);
if (hid_handle != INVALID_HANDLE_VALUE) {
wchar_t product_buffer[126] {};
if (HidD_GetProductString(hid_handle, product_buffer, sizeof(product_buffer))) {
auto const product_str = wchar_to_u8(product_buffer);
if (!product_str.empty() && device_description != product_str) {
device_description += " - " + product_str;
}
}
CloseHandle(hid_handle);
}
}
}
}
// kill it with fire
SetupDiDestroyDeviceInfoList(devinfo);
// some descriptions are empty - especially using WINE
if (device_description.empty()) {
device_description = device_name;
}
// alias
if (device_description == R"(\\?\WINE_MOUSE)") {
device_description = "WINE Mouse";
} else if (device_description == R"(\\?\WINE_KEYBOARD)") {
device_description = "WINE Keyboard";
}
// return result
return device_description;
}
void rawinput::RawInputManager::sextet_register(const std::string &port_name, const std::string &alias,
bool warn) {
log_misc("rawinput", "checking for sextet-stream device on {}...", port_name);
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
// check for any sextet-stream devices
Device device {};
device.type = SEXTET_OUTPUT;
device.name = "sextet_" + port_name;
device.desc = alias + " (" + port_name + ")";
device.sextetInfo = new rawinput::SextetDevice(R"(\\.\)" + port_name);
device.mutex = new std::mutex();
device.mutex_out = new std::mutex();
// try to connect
if (device.sextetInfo->connect()) {
// successful connection
this->devices.emplace_back(device);
// notify add
for (auto &cb : this->callback_add) {
cb.f(cb.data, &this->devices.back());
}
} else if (warn) {
log_warning("rawinput", "unable to connect to {} on {}", alias, port_name);
}
}
void rawinput::RawInputManager::devices_remove(const std::string &name) {
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
// iterate devices
for (auto &device : this->devices) {
// check if name matches
if (device.name == name) {
// remove device
this->devices_destruct(&device);
break;
}
}
}
// close any MIDI handle detached above, now that devices_mutex is released.
// removing a MIDI device queues its handle for deferred close; flush it here
// since we cannot rely on a later scan happening to do it
this->midi_close_deferred_flush();
}
void rawinput::RawInputManager::devices_register() {
// check input window
if (!this->input_hwnd) {
log_warning("rawinput", "trying to register devices without input window");
return;
}
log_misc("rawinput", "registering raw input devices...");
// register keyboard
RAWINPUTDEVICE keyboard_device{};
if (rawinput::NOLEGACY) {
// this prevents win/media/special key events to get sent to the game window
keyboard_device.dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK;
} else {
keyboard_device.dwFlags = RIDEV_INPUTSINK;
}
keyboard_device.usUsagePage = 1;
keyboard_device.usUsage = 0x06;
keyboard_device.hwndTarget = this->input_hwnd;
if (!RegisterRawInputDevices(&keyboard_device, 1, sizeof(keyboard_device))) {
log_warning("rawinput", "failed to register keyboard events: {}", GetLastError());
}
// register keypad
RAWINPUTDEVICE keypad_device{};
keypad_device.dwFlags = RIDEV_INPUTSINK;
keypad_device.usUsagePage = 1;
keypad_device.usUsage = 0x07;
keypad_device.hwndTarget = this->input_hwnd;
if (!RegisterRawInputDevices(&keypad_device, 1, sizeof(keypad_device))) {
log_warning("rawinput", "failed to register keypad events: {}", GetLastError());
}
// register mouse
RAWINPUTDEVICE mouse_device{};
mouse_device.dwFlags = RIDEV_INPUTSINK;
mouse_device.usUsagePage = 1;
mouse_device.usUsage = 0x02;
mouse_device.hwndTarget = this->input_hwnd;
if (!RegisterRawInputDevices(&mouse_device, 1, sizeof(mouse_device))) {
log_warning("rawinput", "failed to register mouse events: {}", GetLastError());
}
// register joystick
RAWINPUTDEVICE joystick_device{};
joystick_device.dwFlags = RIDEV_INPUTSINK;
joystick_device.usUsagePage = 1;
joystick_device.usUsage = 0x04;
joystick_device.hwndTarget = this->input_hwnd;
if (!RegisterRawInputDevices(&joystick_device, 1, sizeof(joystick_device))) {
log_warning("rawinput", "failed to register joystick events: {}", GetLastError());
}
// register gamepad
RAWINPUTDEVICE gamepad_device{};
gamepad_device.dwFlags = RIDEV_INPUTSINK;
gamepad_device.usUsagePage = 1;
gamepad_device.usUsage = 0x05;
gamepad_device.hwndTarget = this->input_hwnd;
if (!RegisterRawInputDevices(&gamepad_device, 1, sizeof(gamepad_device))) {
log_warning("rawinput", "failed to register gamepad events: {}", GetLastError());
}
// register digitizer
RAWINPUTDEVICE digitizer_device{};
digitizer_device.dwFlags = RIDEV_PAGEONLY | RIDEV_INPUTSINK;
digitizer_device.usUsagePage = 0x0D;
digitizer_device.usUsage = 0x00;
digitizer_device.hwndTarget = this->input_hwnd;
if (!RegisterRawInputDevices(&digitizer_device, 1, sizeof(digitizer_device))) {
log_warning("rawinput", "failed to register digitizer events: {}", GetLastError());
}
}
void rawinput::RawInputManager::devices_unregister() {
// unregister keyboard
RAWINPUTDEVICE keyboard_device {};
if (rawinput::NOLEGACY) {
keyboard_device.dwFlags = RIDEV_NOLEGACY | RIDEV_INPUTSINK | RIDEV_REMOVE;
} else {
keyboard_device.dwFlags = RIDEV_INPUTSINK | RIDEV_REMOVE;
}
keyboard_device.usUsagePage = 1;
keyboard_device.usUsage = 0x06;
keyboard_device.hwndTarget = this->input_hwnd;
RegisterRawInputDevices(&keyboard_device, 1, sizeof(keyboard_device));
// unregister keypad
RAWINPUTDEVICE keypad_device {};
keypad_device.dwFlags = RIDEV_INPUTSINK | RIDEV_REMOVE;
keypad_device.usUsagePage = 1;
keypad_device.usUsage = 0x07;
keypad_device.hwndTarget = this->input_hwnd;
RegisterRawInputDevices(&keypad_device, 1, sizeof(keypad_device));
// unregister mouse
RAWINPUTDEVICE mouse_device {};
mouse_device.dwFlags = RIDEV_INPUTSINK | RIDEV_REMOVE;
mouse_device.usUsagePage = 1;
mouse_device.usUsage = 0x02;
mouse_device.hwndTarget = this->input_hwnd;
RegisterRawInputDevices(&mouse_device, 1, sizeof(mouse_device));
// unregister joystick
RAWINPUTDEVICE joystick_device {};
joystick_device.dwFlags = RIDEV_INPUTSINK | RIDEV_REMOVE;
joystick_device.usUsagePage = 1;
joystick_device.usUsage = 0x04;
joystick_device.hwndTarget = this->input_hwnd;
RegisterRawInputDevices(&joystick_device, 1, sizeof(joystick_device));
// unregister gamepad
RAWINPUTDEVICE gamepad_device {};
gamepad_device.dwFlags = RIDEV_INPUTSINK | RIDEV_REMOVE;
gamepad_device.usUsagePage = 1;
gamepad_device.usUsage = 0x06;
gamepad_device.hwndTarget = this->input_hwnd;
RegisterRawInputDevices(&gamepad_device, 1, sizeof(gamepad_device));
// unregister digitizer
RAWINPUTDEVICE digitizer_device {};
digitizer_device.dwFlags = RIDEV_PAGEONLY | RIDEV_INPUTSINK | RIDEV_REMOVE;
digitizer_device.usUsagePage = 0x0D;
digitizer_device.usUsage = 0x00;
digitizer_device.hwndTarget = this->input_hwnd;
RegisterRawInputDevices(&digitizer_device, 1, sizeof(digitizer_device));
}
void rawinput::RawInputManager::devices_destruct() {
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
// dispose devices (if there is anything to dispose)
if (!this->devices.empty()) {
log_info("rawinput", "disposing devices");
for (auto &device : this->devices) {
this->devices_destruct(&device, false);
delete device.mutex;
delete device.mutex_out;
}
// empty array
this->devices.clear();
}
}
// close any MIDI handles detached above, now that devices_mutex is released
this->midi_close_deferred_flush();
}
void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
// check if destroyed
if (device->type == DESTROYED) {
return;
}
// optionally log
if (log) {
log_info("rawinput", "destroying device: {} / {}", device->desc, device->name);
}
// hold the output mutex for the whole teardown, taken before we flip the type
// or free anything. device_write_output() locks mutex_out while dereferencing
// type-specific state (hidInfo/sextetInfo/...) on a snapshot taken outside
// devices_mutex, so mutex_out is what actually serializes it against us
std::lock_guard<std::mutex> lock_out(*device->mutex_out);
// mark as destroyed
auto device_type = device->type;
device->type = DESTROYED;
// notify change
for (auto &cb : this->callback_change) {
cb.f(cb.data, device);
}
/*
* lock device
* note: this is an exception to only locking devices when we acquire them from the list
* callbacks could want to lock the mutex as well and it isn't recursive
* this also means the device must be unlocked before calling this function
*/
std::lock_guard<std::mutex> lock(*device->mutex);
// close device handles
switch (device_type) {
case HID:
if (device->hidInfo->handle != INVALID_HANDLE_VALUE) {
CloseHandle(device->hidInfo->handle);
device->hidInfo->handle = INVALID_HANDLE_VALUE;
}
break;
case MIDI:
// never call midiInReset/midiInClose here: this runs under devices_mutex
// and those WinMM calls block until in-flight input_midi_proc callbacks
// return, callbacks that also take devices_mutex. detach the handle and
// let midi_close_deferred_flush() close it once the lock is released
if (device->handle != (HANDLE) INVALID_HANDLE_VALUE) {
log_misc("rawinput", "deferring MIDI handle close for: {}", device->desc);
this->midi_close_deferred.push_back((HMIDIIN) device->handle);
device->handle = (HANDLE) INVALID_HANDLE_VALUE;
}
break;
case SEXTET_OUTPUT:
device->sextetInfo->disconnect();
break;
default:
break;
}
// clean up generic stuff
delete device->mouseInfo;
device->mouseInfo = nullptr;
delete device->keyboardInfo;
device->keyboardInfo = nullptr;
delete device->hidInfo;
device->hidInfo = nullptr;
delete device->midiInfo;
device->midiInfo = nullptr;
delete device->sextetInfo;
device->sextetInfo = nullptr;
delete device->smxstageInfo;
device->smxstageInfo = nullptr;
delete device->smxdedicabInfo;
device->smxdedicabInfo = nullptr;
// note: mutex and mutex_out are intentionally left alive here. other threads
// may still hold a snapshot pointer to this device and block on them, so they
// are only freed during full teardown; on reuse the slot keeps the same pair
}
LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
HWND hWnd, UINT msg, WPARAM wparam, LPARAM lParam) {
// message switch
switch (msg) {
case WM_CREATE: {
// save reference
auto create_params = reinterpret_cast<LPCREATESTRUCT>(lParam);
SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(create_params->lpCreateParams));
break;
}
case WM_INPUT: {
// get reference
auto ref = reinterpret_cast<RawInputManager *>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
// get raw input data
UINT data_size = 0;
if (GetRawInputData(
(HRAWINPUT) lParam,
RID_INPUT,
nullptr,
&data_size,
sizeof(RAWINPUTHEADER)) == (UINT) -1) {
break;
}
if (!data_size) {
break;
}
std::shared_ptr<RAWINPUT> data((RAWINPUT *) new char[data_size]{});
if (GetRawInputData(
(HRAWINPUT) lParam,
RID_INPUT,
data.get(),
&data_size,
sizeof(RAWINPUTHEADER)) != data_size) {
break;
}
// find device
HANDLE device_handle = data->header.hDevice;
// lock the device list so a concurrent scan can't mutate it while we iterate
std::lock_guard<std::recursive_mutex> devices_lock(ref->devices_mutex);
for (auto &device : ref->devices_get()) {
// skip if this is the wrong device
if (device.handle != device_handle) {
continue;
}
// get input time
const auto input_time = get_performance_seconds();
// lock device
device.mutex->lock();
// update hz
double diff_time = input_time - device.input_time;
if (diff_time > 0.0001) {
device.input_hz = 1.f / diff_time;
device.input_hz_max = MAX(device.input_hz_max, device.input_hz);
device.input_time = input_time;
}
// check type
switch (device.type) {
case DESTROYED:
log_warning("rawinput", "received input msg for destroyed device");
break;
case MOUSE: {
// get mouse data
auto data_mouse = data->data.mouse;
// save position
if (data_mouse.usFlags & MOUSE_MOVE_ABSOLUTE) {
if (device.mouseInfo->pos_x != data_mouse.lLastX) {
device.updated = true;
}
device.mouseInfo->pos_x = data_mouse.lLastX;
if (device.mouseInfo->pos_y != data_mouse.lLastY) {
device.updated = true;
}
device.mouseInfo->pos_y = data_mouse.lLastY;
} else {
if (data_mouse.lLastX != 0 || data_mouse.lLastY != 0) {
device.updated = true;
}
device.mouseInfo->pos_x += data_mouse.lLastX;
device.mouseInfo->pos_y += data_mouse.lLastY;
}
// check buttons
if (data_mouse.usButtonFlags) {
auto &key_states = device.mouseInfo->key_states;
auto &key_up = device.mouseInfo->key_up;
auto &key_down = device.mouseInfo->key_down;
if (data_mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN) {
device.updated = true;
key_states[MOUSEBTN_LEFT] = true;
key_down[MOUSEBTN_LEFT] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_UP) {
device.updated = true;
key_states[MOUSEBTN_LEFT] = false;
key_up[MOUSEBTN_LEFT] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_DOWN) {
device.updated = true;
key_states[MOUSEBTN_RIGHT] = true;
key_down[MOUSEBTN_RIGHT] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_RIGHT_BUTTON_UP) {
device.updated = true;
key_states[MOUSEBTN_RIGHT] = false;
key_up[MOUSEBTN_RIGHT] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_DOWN) {
device.updated = true;
key_states[MOUSEBTN_MIDDLE] = true;
key_down[MOUSEBTN_MIDDLE] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_MIDDLE_BUTTON_UP) {
device.updated = true;
key_states[MOUSEBTN_MIDDLE] = false;
key_up[MOUSEBTN_MIDDLE] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_1_DOWN) {
device.updated = true;
key_states[MOUSEBTN_1] = true;
key_down[MOUSEBTN_1] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_1_UP) {
device.updated = true;
key_states[MOUSEBTN_1] = false;
key_up[MOUSEBTN_1] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_2_DOWN) {
device.updated = true;
key_states[MOUSEBTN_2] = true;
key_down[MOUSEBTN_2] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_2_UP) {
device.updated = true;
key_states[MOUSEBTN_2] = false;
key_up[MOUSEBTN_2] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_3_DOWN) {
device.updated = true;
key_states[MOUSEBTN_3] = true;
key_down[MOUSEBTN_3] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_3_UP) {
device.updated = true;
key_states[MOUSEBTN_3] = false;
key_up[MOUSEBTN_3] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_4_DOWN) {
device.updated = true;
key_states[MOUSEBTN_4] = true;
key_down[MOUSEBTN_4] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_4_UP) {
device.updated = true;
key_states[MOUSEBTN_4] = false;
key_up[MOUSEBTN_4] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_5_DOWN) {
device.updated = true;
key_states[MOUSEBTN_5] = true;
key_down[MOUSEBTN_5] = input_time;
}
if (data_mouse.usButtonFlags & RI_MOUSE_BUTTON_5_UP) {
device.updated = true;
key_states[MOUSEBTN_5] = false;
key_up[MOUSEBTN_5] = input_time;
}
}
// check wheel
if (data_mouse.usButtonFlags & RI_MOUSE_WHEEL) {
if ((short) data_mouse.usButtonData != 0) {
device.updated = true;
}
device.mouseInfo->pos_wheel += ((short) data_mouse.usButtonData) / WHEEL_DELTA;
}
break;
}
case KEYBOARD: {
// get keyboard data
auto &data_keyboard = data->data.keyboard;
// set index based on flags
int index = 0;
if (data_keyboard.Flags & RI_KEY_E0) {
index += 256;
}
if (data_keyboard.Flags & RI_KEY_E1) {
index += 512;
}
// check the funny exceptions
USHORT vkey = data_keyboard.VKey;
switch (index + vkey) {
case 17:
vkey = VK_LCONTROL;
break;
case 273:
vkey = VK_RCONTROL;
break;
}
switch (data_keyboard.MakeCode) {
case 42:
vkey = VK_LSHIFT;
break;
case 54:
vkey = VK_RSHIFT;
break;
}
// update key state
if (vkey < 255) {
bool state = (data_keyboard.Flags & RI_KEY_BREAK) == 0;
auto &cur_state = device.keyboardInfo->key_states[index + vkey];
if (!cur_state && state) {
cur_state = state;
device.updated = true;
device.keyboardInfo->key_down[index + vkey] = input_time;
} else if (cur_state && !state) {
cur_state = state;
device.updated = true;
device.keyboardInfo->key_up[index + vkey] = input_time;
}
}
break;
}
case HID: {
// get HID data
auto &data_hid = data->data.hid;
// parse reports
for (const auto &pair : device.hidInfo->button_usage_pages) {
const auto usage_page = pair.first.first;
const auto link_collection = pair.first.second;
const auto button_count = pair.second;
ULONG usages_length = button_count;
std::vector<USAGE> usages(static_cast<size_t>(usages_length));
if (HidP_GetUsages(
HidP_Input,
usage_page,
link_collection,
usages.data(),
&usages_length,
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
reinterpret_cast<PCHAR>(data_hid.bRawData),
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS) {
// log_warning(
// "rawinput",
// "failed to get usages for device {}, usage page {:x} and link collection {:x}",
// device.desc,
// usage_page, link_collection);
continue;
}
// log_info(
// "rawinput",
// "processing HID input for device {}, usage page {:x} and link collection {:x} with {} buttons, got {} reports",
// device.desc,
// usage_page, link_collection, button_count, usages_length);
// buttons
for (size_t cap_num = 0; cap_num < device.hidInfo->button_caps_list.size(); cap_num++) {
auto &button_caps = device.hidInfo->button_caps_list[cap_num];
auto &button_states = device.hidInfo->button_states[cap_num];
auto &button_down = device.hidInfo->button_down[cap_num];
auto &button_up = device.hidInfo->button_up[cap_num];
// is this the right usage page and link collection?
if (button_caps.UsagePage != usage_page || button_caps.LinkCollection != link_collection) {
continue;
}
// get button count
int button_count = button_caps.Range.UsageMax - button_caps.Range.UsageMin + 1;
if (button_count <= 0) {
continue;
}
// update buttons
std::vector<bool> new_states(button_count);
for (ULONG usage_num = 0; usage_num < usages_length; usage_num++) {
if (usages[usage_num] < button_caps.Range.UsageMin ||
usages[usage_num] > button_caps.Range.UsageMax) {
continue;
}
USAGE usage = usages[usage_num] - button_caps.Range.UsageMin;
// guard against some buggy device sending an event for a usage below `UsageMin`
if (usage < button_count) {
new_states[usage] = true;
}
}
for (int button_num = 0; button_num < button_count; button_num++) {
if (!new_states[button_num] && button_states[button_num]) {
device.updated = true;
button_states[button_num] = new_states[button_num];
button_down[button_num] = input_time;
} else if (new_states[button_num] && !button_states[button_num]) {
device.updated = true;
button_states[button_num] = new_states[button_num];
button_up[button_num] = input_time;
}
}
}
}
// analogs
for (auto cap_num = 0; cap_num < device.hidInfo->caps.NumberInputValueCaps; cap_num++) {
auto &value_caps = device.hidInfo->value_caps_list[cap_num];
// get value
LONG value_raw = 0;
if (HidP_GetUsageValue(
HidP_Input,
value_caps.UsagePage,
value_caps.LinkCollection,
value_caps.Range.UsageMin,
reinterpret_cast<ULONG *>(&value_raw),
reinterpret_cast<PHIDP_PREPARSED_DATA>(device.hidInfo->preparsed_data.get()),
reinterpret_cast<CHAR *>(data_hid.bRawData),
data_hid.dwSizeHid) != HIDP_STATUS_SUCCESS)
{
continue;
}
// get min and max
LONG value_min = value_caps.LogicalMin;
LONG value_max = value_caps.LogicalMax;
float value;
// 0x1 == generic desktop, 0x39 == hat switch
if (value_caps.UsagePage == 0x1 && value_caps.Range.UsageMin == 0x39) {
if (value_min <= value_raw && value_raw <= value_max) {
// scale to float; minimum valid value is UP, and increases in clockwise order
value = (float) (value_raw - value_min) / (float) (value_max - value_min);
} else {
// hat switches report an out-of-bounds value to indicate a neutral position, so it
// needs special handling; here, we will use a negative value to indicate neutral
value = -1.f;
}
} else {
// fix sign bits for signed values
if (value_caps.LogicalMin < 0 &&
0 < value_caps.BitSize && value_caps.BitSize < 32) {
ULONG raw = static_cast<ULONG>(value_raw) & ((1u << value_caps.BitSize) - 1u);
const ULONG sign_bit = 1u << (value_caps.BitSize - 1);
value_raw = static_cast<LONG>((raw ^ sign_bit) - sign_bit);
}
// automatic calibration
if (value_raw < value_min) {
value_caps.LogicalMin = value_raw;
value_min = value_raw;
}
if (value_raw > value_max) {
value_caps.LogicalMax = value_raw;
value_max = value_raw;
}
// scale to float
value = (float) (value_raw - value_min) / (float) (value_max - value_min);
}
// store value
auto &cur_state = device.hidInfo->value_states[cap_num];
if (cur_state != value) {
device.updated = true;
cur_state = value;
}
// store raw value
auto &cur_raw_state = device.hidInfo->value_states_raw[cap_num];
if (cur_raw_state != value_raw) {
device.updated = true;
cur_raw_state = value_raw;
}
}
// touch screen
rawinput::touch::update_input(&device);
break;
}
default:
break;
}
// free device
device.mutex->unlock();
// don't iterate through the other devices
break;
}
// update controller state ring buffers (DDR/MDXF)
mdxf_poll(true);
// call the default window handler for cleanup
DefWindowProc(hWnd, msg, wparam, lParam);
// return zero to indicate the event was processed
return 0;
}
case WM_DEVICECHANGE: {
// call hotplug manager
auto ref = reinterpret_cast<RawInputManager *>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
if (ref != nullptr && ref->hotplug != nullptr) {
return ref->hotplug->WndProc(hWnd, msg, wparam, lParam);
}
break;
}
default:
break;
}
// default
return DefWindowProc(hWnd, msg, wparam, lParam);
}
void CALLBACK rawinput::RawInputManager::input_midi_proc(HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance,
DWORD_PTR dwParam1, DWORD_PTR dwParam2) {
// get instance
auto ri_mgr = reinterpret_cast<RawInputManager *>(dwInstance);
// handle message
switch (wMsg) {
case MIM_OPEN:
case MIM_CLOSE:
break;
case MIM_MOREDATA:
case MIM_DATA: {
// lock the device list so a concurrent scan can't mutate it while we iterate
std::lock_guard<std::recursive_mutex> devices_lock(ri_mgr->devices_mutex);
// param mapping
auto dwMidiMessage = dwParam1;
//auto dwTimestamp = dwParam2;
// message unpacking
auto midi_status = LOBYTE(LOWORD(dwMidiMessage));
auto midi_status_command = (midi_status & 0xF0u) >> 4u;
auto midi_status_channel = (midi_status & 0x0Fu);
auto midi_byte1 = HIBYTE(LOWORD(dwMidiMessage));
auto midi_byte2 = LOBYTE(HIWORD(dwMidiMessage));
// callbacks
for (auto &callback : ri_mgr->callback_midi) {
// find device
for (auto &device : ri_mgr->devices_get()) {
if (device.type == MIDI && device.handle == hMidiIn) {
// call function
callback.f(callback.data, &device,
midi_status_command, midi_status_channel,
midi_byte1, midi_byte2);
}
}
}
// skip unused messages types early for performance
bool skip = false;
switch (midi_status_command) {
case 0xA: // POLYPHONIC PRESSURE
case 0xC: // PROGRAM CHANGE
case 0xD: // CHANNEL PRESSURE
case 0xF: // SYSTEM EXCLUSIVE
skip = true;
break;
default:
break;
}
if (skip) {
break;
}
// find device
for (auto &device : ri_mgr->devices_get()) {
// filter non MIDI devices
if (device.type != MIDI) {
continue;
}
// filter wrong handles
if (device.handle != hMidiIn) {
continue;
}
// get input time
const auto input_time = get_performance_seconds();
// lock device
std::lock_guard<std::mutex> lock(*device.mutex);
// update hz
auto diff_time = input_time - device.input_time;
if (diff_time > 0.0001) {
device.input_hz = 1.f / diff_time;
device.input_hz_max = MAX(device.input_hz_max, device.input_hz);
device.input_time = input_time;
}
// command logic
switch (midi_status_command) {
case 0x8: { // NOTE OFF
// param mapping
const auto midi_note = midi_byte1 & 127u;
// log_misc("midi", "[{}] OFF", midi_note);
// get index
const auto midi_index = midi_status_channel * 128 + midi_note;
if (midi_index < 16 * 128) {
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
// update velocity
device.midiInfo->velocity[midi_index] = 0;
// disable note
if (device.midiInfo->states_events[midi_index]) {
device.midiInfo->states[midi_index] = false;
}
device.updated = true;
} else {
// v2 logic
// exactly the same as NOTE ON with 0 velocity
// velocity is kept; api will ignore it if button is not pressed
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
device.midiInfo->v2_last_off_time[midi_index] = get_performance_milliseconds();
device.updated = true;
}
// for v2_drum, NOTE OFF is ignored
}
}
break;
}
case 0x9: { // NOTE ON
// param mapping
const auto midi_note = midi_byte1 & 127u;
// per MIDI spec, if NOTE ON is sent with 0 velocity, it's the same thing as NOTE OFF.
const auto midi_velocity = midi_byte2 & 127u;
// log_misc("midi", "[{}] ON v={}", midi_note, midi_velocity);
// get index
const auto midi_index = midi_status_channel * 128 + midi_note;
if (midi_index < 16 * 128) {
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
// update velocity
device.midiInfo->velocity[midi_index] = (uint8_t) midi_velocity;
if (midi_velocity) {
// update events (for legacy logic)
// how does this work? see the comment in api.cpp around the check for
// get_midi_algorithm() for an explanation
// so currently it's meant to be turned on
device.midiInfo->states[midi_index] = true;
// if its already on just increase it by one to turn it off
if (device.midiInfo->states_events[midi_index] % 2)
device.midiInfo->states_events[midi_index]++;
else
device.midiInfo->states_events[midi_index] += 2;
} else if (!device.midiInfo->freeze) {
// velocity 0 means turn it off
device.midiInfo->states[midi_index] = false;
}
device.updated = true;
} else {
// v2 logic
const auto now = get_performance_milliseconds();
auto threshold = device.midiInfo->v2_velocity_threshold[midi_index];
// when device is frozen (binding is happening) ignore the velocity threshold
// this allows users to bind keys even if the midi note is set to high threshold at
// rawinput layer, either from a previous binding that was cleared, or existing binding
// for another button
if (device.midiInfo->freeze) {
threshold = 0;
}
if (threshold < midi_velocity) {
device.midiInfo->velocity[midi_index] = (uint8_t)midi_velocity;
device.midiInfo->v2_last_on_time[midi_index] = now;
// disable holds and release all notes immediately
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2_DRUM) {
device.midiInfo->v2_last_off_time[midi_index] = now;
}
device.updated = true;
} else {
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::V2) {
// insufficient velocity ON == exactly the same as NOTE OFF
device.midiInfo->v2_last_off_time[midi_index] = now;
device.updated = true;
}
// for v2_drum, NOTE ON with insufficient velocity is ignored
}
}
}
break;
}
case 0xA: // POLYPHONIC PRESSURE
break; // skipped above (!)
case 0xB: { // CONTROL CHANGE
// param mapping
auto midi_control = midi_byte1 & 127;
auto midi_value = midi_byte2 & 127u;
// get index
auto channel_offset = midi_status_channel * 128;
auto midi_index = channel_offset + midi_control;
if (midi_index < 16 * 128) {
// continuous controller MSB
if (midi_control >= 0x00 && midi_control <= 0x1F) {
// update index
midi_index = midi_status_channel * 32 + midi_control;
device.midiInfo->controls_precision_set[midi_index] = true;
// check if MSB wasn't sent yet
if (!device.midiInfo->controls_precision_msb[midi_index]) {
device.midiInfo->controls_precision_msb[midi_index] = true;
// move LSB value to actual position
device.midiInfo->controls_precision[midi_index] >>= 7u;
}
// update MSB
auto tmp = device.midiInfo->controls_precision[midi_index];
tmp = (tmp & 127u) | midi_value << 7u;
if (!device.midiInfo->controls_precision_lsb[midi_index])
tmp = (tmp & (127u << 7u)) | midi_value;
if (device.midiInfo->controls_precision[midi_index] != tmp) {
device.midiInfo->controls_precision[midi_index] = tmp;
device.updated = true;
}
}
// continuous controller LSB
else if (midi_control >= 0x20 && midi_control <= 0x3F) {
// update index
midi_index = midi_status_channel * 32 + midi_control - 0x20;
device.midiInfo->controls_precision_set[midi_index] = true;
device.midiInfo->controls_precision_lsb[midi_index] = true;
// check for MSB flag
if (device.midiInfo->controls_precision_msb[midi_index]) {
// update LSB only
auto tmp = device.midiInfo->controls_precision[midi_index];
tmp &= 127u << 7u;
tmp |= midi_value;
if (device.midiInfo->controls_precision[midi_index] != tmp) {
device.midiInfo->controls_precision[midi_index] = tmp;
device.updated = true;
}
} else {
// cast to MSB
if (device.midiInfo->controls_precision[midi_index] != midi_value << 7u) {
device.midiInfo->controls_precision[midi_index] = midi_value << 7u | midi_value;
device.updated = true;
}
}
}
// on/off controls
else if (midi_control >= 0x40 && midi_control <= 0x45) {
// update index
midi_index = midi_status_channel * 6 + midi_control - 0x40;
device.midiInfo->controls_onoff_set[midi_index] = true;
// get on/off state
const auto onoff_state = midi_value >= 64;
// update device
if (MIDI_NOTE_ALGORITHM == MidiNoteAlgorithm::LEGACY) {
if (device.midiInfo->controls_onoff[midi_index] != onoff_state) {
device.midiInfo->controls_onoff[midi_index] = onoff_state;
device.updated = true;
}
} else {
// v2 and v2_drum:
// unlike notes (drum pads), controls can send continuous ON signal
// therefore, check for rising and falling edges
const auto now = get_performance_milliseconds();
const auto previous_value = device.midiInfo->controls_onoff[midi_index];
if (!previous_value && onoff_state) {
device.midiInfo->v2_controls_onoff_last_on_time[midi_index] = now;
device.updated = true;
} else if (previous_value && !onoff_state) {
device.midiInfo->v2_controls_onoff_last_off_time[midi_index] = now;
device.updated = true;
}
device.midiInfo->controls_onoff[midi_index] = onoff_state;
}
}
// single byte controllers
else if (midi_control >= 0x46 && midi_control <= 0x5F) {
// update index
midi_index = midi_status_channel * 44 + midi_control - 0x46;
device.midiInfo->controls_single_set[midi_index] = true;
// update device
if (device.midiInfo->controls_single[midi_index] != midi_value) {
device.midiInfo->controls_single[midi_index] = midi_value;
device.updated = true;
}
}
// increment/decrement and parameter numbers
else if (midi_control >= 0x60 && midi_control <= 0x65) {
// skip
}
// undefined single-byte controllers
else if (midi_control >= 0x66 && midi_control <= 0x77) {
// update index
auto sbc_count = 0x5F - 0x46 + 1;
midi_index = midi_status_channel * 44 + midi_control - 0x66 + sbc_count;
device.midiInfo->controls_single_set[midi_index] = true;
// update device
if (device.midiInfo->controls_single[midi_index] != midi_value) {
device.midiInfo->controls_single[midi_index] = midi_value;
device.updated = true;
}
}
// channel mode messages
else if (midi_control >= 0x78 && midi_control <= 0x7F) {
switch (midi_control) {
case 0x78: // all sound off
break;
case 0x79: { // reset all controllers
for (int i = 0; i < 32; i++)
device.midiInfo->controls_precision[midi_status_channel * 32 + i] = 0;
for (int i = 0; i < 44; i++)
device.midiInfo->controls_single[midi_status_channel * 44 + i] = 0;
for (int i = 0; i < 6; i++) {
const auto index = midi_status_channel * 6 + i;
device.midiInfo->controls_onoff[index] = false;
device.midiInfo->v2_controls_onoff_last_on_time[index] = 0;
device.midiInfo->v2_controls_onoff_last_off_time[index] = 0;
}
device.updated = true;
break;
}
case 0x7A: // local control on/off
break;
case 0x7B: // all notes off
case 0x7C: // omni mode off + all notes off
case 0x7D: // omni mode on + all notes off
case 0x7E: // mono mode on + poly off + all notes off
case 0x7F: // poly mode on + mono off + all notes off
for (int i = 0; i < 128; i++) {
// common
device.midiInfo->velocity[channel_offset + i] = 0;
device.midiInfo->bind_states[channel_offset + i] = false;
// legacy
device.midiInfo->states[channel_offset + i] = false;
device.midiInfo->states_events[channel_offset + i] = 0;
// v2
device.midiInfo->v2_last_off_time[channel_offset + i] = 0.0;
device.midiInfo->v2_last_on_time[channel_offset + i] = 0.0;
}
device.updated = true;
break;
default:
break;
}
break;
}
}
break;
}
case 0xC: // PROGRAM CHANGE
break; // skipped above (!)
case 0xD: // CHANNEL PRESSURE
break; // skipped above (!)
case 0xE: { // PITCH BENDING
// raw values range from [0, 0x3FFF] (16383)
// build value, centered around zero [-8192, 8191]
int16_t value = ((midi_byte1) | (midi_byte2 << 7u)) - 0x2000;
// update device
if (device.midiInfo->pitch_bend[midi_status_channel] != value) {
device.midiInfo->pitch_bend[midi_status_channel] = value;
device.midiInfo->pitch_bend_set[midi_status_channel] = true;
device.updated = true;
}
break;
}
case 0xF: // SYSTEM EXCLUSIVE
break; // skipped above (!)
default:
break;
}
// don't iterate through the other devices
break;
}
break;
}
case MIM_LONGDATA:
case MIM_ERROR:
case MIM_LONGERROR:
break;
default:
break;
}
}
void rawinput::RawInputManager::device_write_output(Device *device, bool only_updated) {
// check if output is enabled
if (!device->output_enabled) {
return;
}
// check if output is pending
if (only_updated && !device->output_pending) {
return;
}
// lock device
device->mutex_out->lock();
// mark device as updated
device->output_pending = false;
// check device type
switch (device->type) {
case HID: {
// get HID info
auto hid = device->hidInfo;
// check handle
if (hid->handle == INVALID_HANDLE_VALUE) {
break;
}
// check driver
switch (hid->driver) {
case HIDDriver::Default: {
// allocate report
CHAR *report_data = new CHAR[hid->caps.OutputReportByteLength] {};
// set buttons
for (size_t cap_no = 0; cap_no < hid->button_output_caps_list.size(); cap_no++) {
auto &button_cap = hid->button_output_caps_list[cap_no];
auto &button_state_list = hid->button_output_states[cap_no];
// determine which buttons to turn on
std::vector<USAGE> usage_list;
std::vector<USAGE> usage_off_list;
usage_list.reserve(button_state_list.size());
usage_off_list.reserve(button_state_list.size());
for (size_t state_no = 0; state_no < button_state_list.size(); state_no++) {
if (button_state_list[state_no]) {
usage_list.push_back(button_cap.Range.UsageMin + (USAGE) state_no);
} else {
usage_off_list.push_back(button_cap.Range.UsageMin + (USAGE) state_no);
}
}
// set the buttons
auto usage_list_length = (ULONG) usage_list.size();
while (HidP_SetButtons(
HidP_Output,
button_cap.UsagePage,
button_cap.LinkCollection,
&usage_list[0],
&usage_list_length,
reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()),
report_data,
hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) {
// flush report
HidD_SetOutputReport(hid->handle, report_data, hid->caps.OutputReportByteLength);
memset(report_data, 0, hid->caps.OutputReportByteLength);
}
// clear the buttons
auto usage_off_list_length = (ULONG) usage_off_list.size();
while (HidP_UnsetButtons(
HidP_Output,
button_cap.UsagePage,
button_cap.LinkCollection,
&usage_off_list[0],
&usage_off_list_length,
reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()),
report_data,
hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) {
// flush report
DWORD written_bytes = 0;
WriteFile(
hid->handle,
reinterpret_cast<void *>(report_data),
hid->caps.OutputReportByteLength,
&written_bytes,
nullptr
);
memset(report_data, 0, hid->caps.OutputReportByteLength);
}
}
// set values
for (size_t cap_no = 0; cap_no < hid->value_output_caps_list.size(); cap_no++) {
auto &value_cap = hid->value_output_caps_list[cap_no];
auto &value_state = hid->value_output_states[cap_no];
// adjust output value per "brightness" setting
auto adjusted_value_state = value_state * HID_LIGHT_BRIGHTNESS / 100;
// build value
LONG usage_value = value_cap.LogicalMin +
lroundf((value_cap.LogicalMax - value_cap.LogicalMin) * adjusted_value_state);
if (usage_value > value_cap.LogicalMax) {
usage_value = value_cap.LogicalMax;
} else if (usage_value < value_cap.LogicalMin) {
usage_value = value_cap.LogicalMin;
}
// set the state
while (HidP_SetUsageValue(
HidP_Output,
value_cap.UsagePage,
value_cap.LinkCollection,
value_cap.NotRange.Usage,
static_cast<ULONG>(usage_value),
reinterpret_cast<PHIDP_PREPARSED_DATA>(hid->preparsed_data.get()),
report_data,
hid->caps.OutputReportByteLength) == HIDP_STATUS_INCOMPATIBLE_REPORT_ID) {
// flush report
DWORD written_bytes = 0;
WriteFile(
hid->handle,
reinterpret_cast<void *>(report_data),
hid->caps.OutputReportByteLength,
&written_bytes,
nullptr
);
memset(report_data, 0, hid->caps.OutputReportByteLength);
}
}
// MiniMaid Madness
if (hid->attributes.VendorID == 0xBEEF && hid->attributes.ProductID == 0x5730) {
if (hid->caps.OutputReportByteLength >= 8) {
/*
* MiniMaid HID Index Positions:
* 0: HID Report ID
* 1: EXT Values
* 2: Cabinet
* 3: Player 1
* 4: Player 2
* 5: Bass
* 6: Onboard LED Brightness
* 7: Keyboard Enable
* 8: Unused 'hax' variable
*/
// put pads in proper mode
// bit 4 high is pad enable.
report_data[3] |= 0x10u; // P1
report_data[4] |= 0x10u; // P2
// enable keyboard flag
report_data[7] |= 0x01u;
}
}
// write final report
DWORD written_bytes = 0;
WriteFile(
hid->handle,
reinterpret_cast<void *>(report_data),
hid->caps.OutputReportByteLength,
&written_bytes,
nullptr
);
// delete report
delete[] report_data;
break;
}
case HIDDriver::PacDrive: {
// allocate report
uint8_t report_data[5] {};
// set leds
static const size_t mapping[] = {
8, 9, 10, 11, 12, 13, 14, 15,
0, 1, 2, 3, 4, 5, 6, 7
};
auto led_data = (uint16_t *) &report_data[3];
size_t count = 0;
for (auto &button_output_states : hid->button_output_states) {
for (auto &&button_output_state : button_output_states) {
if (button_output_state) {
*led_data |= 1u << mapping[count];
}
count++;
}
}
// write report
DWORD written_bytes = 0;
WriteFile(
hid->handle,
static_cast<void *>(&report_data),
sizeof(report_data),
&written_bytes,
nullptr
);
break;
}
default:
break;
}
break;
}
case SEXTET_OUTPUT: {
device->sextetInfo->push_light_state();
break;
}
case SMX_STAGE: {
device->smxstageInfo->Update();
break;
};
case SMX_DEDICAB: {
device->smxdedicabInfo->Update();
break;
}
case XINPUT_GAMEPAD: {
// nothing - updates to these are instant
break;
}
default:
break;
}
// unlock device
device->mutex_out->unlock();
}
void rawinput::RawInputManager::devices_flush_output(bool optimized) {
// optimized routine
if (optimized) {
// notify thread
output_thread_ready = true;
output_thread_cv.notify_one();
return;
}
// blocking routine
this->devices_write_output_snapshot(false);
}
void rawinput::RawInputManager::devices_write_output_snapshot(bool only_updated) {
// snapshot device pointers under devices_mutex, then write without holding it.
// the std::list keeps addresses stable, so the (potentially blocking) writes
// below don't hold devices_mutex against input
std::vector<Device *> snapshot;
{
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
snapshot.reserve(this->devices.size());
for (auto &device : this->devices) {
snapshot.push_back(&device);
}
}
for (auto *device : snapshot) {
device_write_output(device, only_updated);
}
}
void rawinput::RawInputManager::devices_print() {
if (!DUMP_HID_DEVICES_TO_LOG) {
log_info("rawinput", "verbose dump of HID devices is disabled by default; see -sysdump option");
return;
}
bool touchscreen_found = false;
// lock the device list while iterating
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
// iterate devices
log_info("rawinput", "printing list of detected devices");
log_info("rawinput", "detected device count: {}", devices.size());
for (auto &device : devices) {
bool is_touchscreen = false;
// lock it
device.mutex->lock();
// general information
log_misc("rawinput", "--------begin device @{}", device.handle);
log_misc("rawinput", "device name: {}", device.name);
log_misc("rawinput", "device desc: {}", device.desc);
log_misc("rawinput", "device handle: {}", device.handle);
// type specific
switch (device.type) {
case MOUSE:
log_misc("rawinput", "device type: MOUSE");
break;
case KEYBOARD:
log_misc("rawinput", "device type: KEYBOARD");
break;
case HID: {
log_misc("rawinput", "device type: HID");
log_misc("rawinput", "device preparsed size: {}", device.hidInfo->preparsed_size);
log_misc("rawinput", "device usage: {}", device.hidInfo->usage_name);
// check touchscreen
if (device.hidInfo->touch.valid) {
log_info("rawinput", "device is marked as touchscreen");
if (!touchscreen_found) {
touchscreen_found = true;
is_touchscreen = true;
}
}
// button caps
log_misc("rawinput", "device button caps count: {}",
device.hidInfo->button_caps_list.size());
int button_name_index = 0;
for (size_t i = 0; i < device.hidInfo->button_caps_list.size(); i++) {
auto &button_caps = device.hidInfo->button_caps_list[i];
USAGE usage_min = button_caps.Range.UsageMin;
USAGE usage_max = button_caps.Range.UsageMax;
int cap_len = usage_max - usage_min;
auto &name1 = device.hidInfo->button_caps_names[button_name_index];
auto &name2 = device.hidInfo->button_caps_names[button_name_index + cap_len];
button_name_index += cap_len + 1;
log_misc("rawinput", "device button caps detected: {} to {} ({}-{})",
name1, name2, usage_min, usage_max);
}
// button output caps
log_misc("rawinput", "device button output caps count: {}",
device.hidInfo->button_output_caps_list.size());
int button_output_name_index = 0;
for (size_t i = 0; i < device.hidInfo->button_output_caps_list.size(); i++) {
auto &button_caps = device.hidInfo->button_output_caps_list[i];
USAGE usage_min = button_caps.Range.UsageMin;
USAGE usage_max = button_caps.Range.UsageMax;
int cap_len = usage_max - usage_min;
auto &name1 = device.hidInfo->button_output_caps_names[button_output_name_index];
auto &name2 = device.hidInfo->button_output_caps_names[button_output_name_index + cap_len];
button_output_name_index += cap_len + 1;
log_misc("rawinput", "device button output caps detected: {} to {} ({}-{})",
name1, name2, usage_min, usage_max);
}
// value caps
if (!device.hidInfo->value_caps_list.empty()) {
log_misc("rawinput", "device value caps count: {}",
device.hidInfo->value_caps_list.size());
for (size_t i = 0; i < device.hidInfo->value_caps_list.size(); i++) {
auto &value_caps = device.hidInfo->value_caps_list[i];
if (device.hidInfo->value_caps_names.size() < i) {
log_fatal("rawinput", "value cap has no name!");
}
auto &name = device.hidInfo->value_caps_names[i];
LONG min = value_caps.LogicalMin;
LONG max = value_caps.LogicalMax;
log_misc("rawinput", "device value caps detected: {} ({} to {}, {}-bit)",
name, min, max, value_caps.BitSize);
if (name.compare("X") == 0 && is_touchscreen) {
TOUCHSCREEN_RANGE_X = max;
} else if (name.compare("Y") == 0 && is_touchscreen) {
TOUCHSCREEN_RANGE_Y = max;
}
}
}
// value output caps
if (!device.hidInfo->value_output_caps_list.empty()) {
log_misc("rawinput", "device value output caps count: {}",
device.hidInfo->value_output_caps_list.size());
for (size_t i = 0; i < device.hidInfo->value_output_caps_list.size(); i++) {
auto &value_caps = device.hidInfo->value_output_caps_list[i];
if (device.hidInfo->value_output_caps_names.size() < i) {
log_fatal("rawinput", "value output cap has no name!");
}
auto &name = device.hidInfo->value_output_caps_names[i];
LONG min = value_caps.LogicalMin;
LONG max = value_caps.LogicalMax;
log_misc("rawinput", "device value output caps detected: {} ({} to {}, {}-bit)",
name, min, max, value_caps.BitSize);
}
}
break;
}
case MIDI: {
log_misc("rawinput", "device type: MIDI");
break;
}
case SEXTET_OUTPUT: {
log_misc("rawinput", "device type: SEXTET_OUTPUT");
break;
}
case PIUIO_DEVICE: {
log_misc("rawinput", "device type: PIUIO");
break;
}
case SMX_STAGE: {
log_misc("rawinput", "device type: SMX_STAGE");
break;
}
case SMX_DEDICAB: {
log_misc("rawinput", "device type: SMX_DEDICAB");
break;
}
case XINPUT_GAMEPAD: {
log_misc("rawinput", "device type: XINPUT");
break;
}
case UNKNOWN:
default:
log_warning("rawinput", "device type: UNKNOWN");
break;
}
log_misc("rawinput", "----------end device @{}", device.handle);
// unlock device
device.mutex->unlock();
}
// mark as done
log_misc("rawinput", "done printing devices");
}
rawinput::DeviceInfo rawinput::RawInputManager::get_device_info(const std::string &device_name) {
DeviceInfo info {};
// check device name
if (device_name.size() < 16) {
return info;
}
// remove header
auto name = device_name.substr(4);
// split
std::vector<std::string> elements;
strsplit(name, elements, '#');
// check split
if (elements.size() < 4) {
return info;
}
// fill out fields
info.devclass = elements[0];
info.subclass = elements[1];
info.protocol = elements[2];
info.guid_str = elements[3];
// generate GUID
std::wstring guid_wstr = s2ws(info.guid_str);
if (IIDFromString(guid_wstr.c_str(), &info.guid) != S_OK) {
return info;
}
return info;
}
rawinput::Device *rawinput::RawInputManager::devices_get(const std::string &name, bool updated) {
// if the device name is empty, we do not even have to look for it
if (name.empty()) {
return nullptr;
}
// lock the device list so a concurrent scan can't mutate it while we search.
// the returned pointer stays valid after unlock because devices is a std::list
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
// check if caller wants only updated devices
if (updated) {
// iterate the devices
for (auto &device : this->devices) {
// check if the device names match
if (device.name == name) {
// lock the device since we are messing with updated
device.mutex->lock();
// was the device updated?
if (device.updated) {
// next call shouldn't trigger
device.updated = false;
// unlock the device
device.mutex->unlock();
// return the device
return &device;
} else {
// unlock the device again
device.mutex->unlock();
// return null since the device wasn't updated
return nullptr;
}
}
}
} else {
// just the usual "lookup by name"
for (auto &device : this->devices) {
if (device.name == name) {
return &device;
}
}
}
// device not found
return nullptr;
}
void rawinput::RawInputManager::add_callback_add(void *data, std::function<void (void *, Device *)> callback) {
this->callback_add.push_back(DeviceCallback {
.data = data,
.f = std::move(callback),
});
}
void rawinput::RawInputManager::remove_callback_add(void *data, const std::function<void (void *, Device *)> &callback) {
this->callback_add.erase(std::remove_if(
this->callback_add.begin(), this->callback_add.end(),
[data, callback](DeviceCallback const &cb) {
return cb.data == data && cb.f.target<void>() == callback.target<void>();
}), this->callback_add.end());
}
void rawinput::RawInputManager::add_callback_change(void *data, std::function<void (void *, Device *)> callback) {
this->callback_change.push_back(DeviceCallback {
.data = data,
.f = std::move(callback),
});
}
void rawinput::RawInputManager::remove_callback_change(void * data, const std::function<void (void *, Device *)> &callback) {
this->callback_change.erase(std::remove_if(
this->callback_change.begin(), this->callback_change.end(),
[data, callback](DeviceCallback const &cb) {
return cb.data == data && cb.f.target<void>() == callback.target<void>();
}), this->callback_change.end());
}
void rawinput::RawInputManager::add_callback_midi(void * data, std::function<void (void *, Device *,
uint8_t, uint8_t, uint8_t, uint8_t)> callback) {
this->callback_midi.push_back(MidiCallback {
.data = data,
.f = std::move(callback),
});
}
void rawinput::RawInputManager::remove_callback_midi(void * data, const std::function<void (void *, Device *,
uint8_t, uint8_t, uint8_t, uint8_t)> &callback) {
this->callback_midi.erase(std::remove_if(
this->callback_midi.begin(), this->callback_midi.end(),
[data, callback](MidiCallback const &cb) {
return cb.data == data && cb.f.target<void>() == callback.target<void>();
}), this->callback_midi.end());
}