Files
spice2x.github.io/src/spice2x/rawinput/hotplug.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

139 lines
5.3 KiB
C++

#include <initguid.h>
#include "hotplug.h"
#include <dbt.h>
#include <devguid.h>
#include "misc/eamuse.h"
#include "rawinput.h"
#include "util/fileutils.h"
#include "util/logging.h"
namespace rawinput {
DEFINE_GUID(GUID_HID, 0x4D1E55B2L, 0xF16F, 0x11CF, 0x88, 0xCB, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30);
HotplugManager::HotplugManager(RawInputManager *ri_mgr, HWND hWnd) : ri_mgr(ri_mgr) {
// init settings
DEV_BROADCAST_DEVICEINTERFACE settings_hid {
.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE),
.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE,
.dbcc_reserved = 0,
.dbcc_classguid = GUID_HID,
.dbcc_name = {0},
};
// register notifications
this->hotplug_hid = RegisterDeviceNotification(hWnd, &settings_hid, DEVICE_NOTIFY_WINDOW_HANDLE);
if (this->hotplug_hid == nullptr) {
log_warning("hotplug", "failed to register HID notifications: {}", GetLastError());
}
}
HotplugManager::~HotplugManager() {
// unregister notifications
if (this->hotplug_hid != nullptr) {
UnregisterDeviceNotification(this->hotplug_hid);
}
}
LRESULT CALLBACK HotplugManager::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
// check for device change
if (msg == WM_DEVICECHANGE) {
// check type
switch (wParam) {
case DBT_DEVICEARRIVAL: {
// check arrived device type
auto hdr = (DEV_BROADCAST_HDR*) lParam;
switch (hdr->dbch_devicetype) {
case DBT_DEVTYP_DEVICEINTERFACE: {
auto dev = (const DEV_BROADCAST_DEVICEINTERFACE *) hdr;
this->ri_mgr->midi_scan_start();
// check if class is not HID
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
break;
// hotplug
std::string name(dev->dbcc_name);
this->ri_mgr->devices_scan_rawinput(name);
this->ri_mgr->devices_register();
break;
}
case DBT_DEVTYP_VOLUME: {
auto dev = (const DEV_BROADCAST_VOLUME *) hdr;
auto unitmask = dev->dbcv_unitmask;
// check drive
unsigned long bit;
while (_BitScanForward(&bit, unitmask)) {
// remove drive from unitmask so it is not scanned again
unitmask &= ~(1 << bit);
// convert bit to drive letter char
char drive = 'A' + bit;
log_info("hotplug", "detected volume arrival: {}", drive);
#ifndef SPICETOOLS_SPICECFG_STANDALONE
// auto insert cards
for (int player = 0; player <= 1; player++) {
std::string path = to_string(drive) + ":\\card" + to_string(player) + ".txt";
if (fileutils::file_exists(path)) {
uint8_t card_data[8];
if (eamuse_get_card_from_file(path, card_data, player)) {
eamuse_card_insert(player, card_data);
}
}
}
#endif
}
break;
}
}
// success
return TRUE;
}
case DBT_DEVICEREMOVECOMPLETE: {
// check arrived device type
auto hdr = (DEV_BROADCAST_HDR*) lParam;
switch (hdr->dbch_devicetype) {
case DBT_DEVTYP_DEVICEINTERFACE: {
auto dev = (DEV_BROADCAST_DEVICEINTERFACE*) hdr;
std::string name(dev->dbcc_name);
// destruct device
log_misc("hotplug", "device interface removal: {}", name);
this->ri_mgr->devices_remove(name);
}
}
// success
return TRUE;
}
case DBT_DEVNODES_CHANGED: {
// catch-all device-tree change: MIDI and XInput have no targeted
// arrival/removal notification, so this is our only hook to detect them
// can be a little noisy as it gets called on every device
log_misc("hotplug", "device tree changed, rescanning MIDI + XInput");
this->ri_mgr->midi_scan_start();
this->ri_mgr->devices_scan_xinput();
break;
}
}
}
// default
return DefWindowProc(hwnd, msg, wParam, lParam);
}
}