mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20:42 -07:00
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.
This commit is contained in:
@@ -411,6 +411,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
|
||||
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
|
||||
break;
|
||||
}
|
||||
case rawinput::DESTROYED:
|
||||
// device was unplugged. release the button instead of leaving
|
||||
// state at getLastState(), which would latch a held button on
|
||||
// forever (e.g. a MIDI note held while the device is removed)
|
||||
state = BUTTON_NOT_PRESSED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace rawinput {
|
||||
switch (hdr->dbch_devicetype) {
|
||||
case DBT_DEVTYP_DEVICEINTERFACE: {
|
||||
auto dev = (const DEV_BROADCAST_DEVICEINTERFACE *) hdr;
|
||||
this->ri_mgr->devices_scan_midi();
|
||||
this->ri_mgr->midi_scan_start();
|
||||
|
||||
// check if class is not HID
|
||||
if (memcmp(&dev->dbcc_classguid, &GUID_HID, sizeof(GUID_HID)) != 0)
|
||||
@@ -112,6 +112,7 @@ namespace rawinput {
|
||||
std::string name(dev->dbcc_name);
|
||||
|
||||
// destruct device
|
||||
log_misc("hotplug", "device interface removal: {}", name);
|
||||
this->ri_mgr->devices_remove(name);
|
||||
}
|
||||
}
|
||||
@@ -120,8 +121,11 @@ namespace rawinput {
|
||||
return TRUE;
|
||||
}
|
||||
case DBT_DEVNODES_CHANGED: {
|
||||
// TODO: this can be a little noisy as it gets called on every device
|
||||
this->ri_mgr->devices_scan_midi();
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,24 @@ namespace rawinput {
|
||||
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;
|
||||
}
|
||||
@@ -100,6 +118,9 @@ void rawinput::RawInputManager::stop() {
|
||||
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();
|
||||
|
||||
@@ -182,12 +203,18 @@ void rawinput::RawInputManager::input_hwnd_destroy() {
|
||||
}
|
||||
|
||||
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();
|
||||
this->devices_scan_midi();
|
||||
|
||||
// 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) {
|
||||
@@ -207,7 +234,99 @@ void rawinput::RawInputManager::devices_reload() {
|
||||
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
|
||||
@@ -239,6 +358,7 @@ void rawinput::RawInputManager::devices_scan_rawinput(const std::string &device_
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -262,11 +382,13 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
|
||||
|
||||
// check for duplicate handle
|
||||
size_t unique_id = devices.size() + 1;
|
||||
for (size_t i = 0; i < this->devices.size(); i++) {
|
||||
if (this->devices[i].name == device_name) {
|
||||
size_t i = 0;
|
||||
for (const auto &existing : this->devices) {
|
||||
if (existing.name == device_name) {
|
||||
unique_id = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// build device
|
||||
@@ -804,8 +926,9 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *device
|
||||
// carry over old device ID
|
||||
new_device.id = prev_device.id;
|
||||
|
||||
// destruct and replace
|
||||
// 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
|
||||
@@ -832,6 +955,15 @@ void rawinput::RawInputManager::devices_scan_rawinput(RAWINPUTDEVICELIST *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++) {
|
||||
@@ -845,6 +977,37 @@ void rawinput::RawInputManager::devices_scan_midi() {
|
||||
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,
|
||||
@@ -858,6 +1021,9 @@ void rawinput::RawInputManager::devices_scan_midi() {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -891,37 +1057,34 @@ void rawinput::RawInputManager::devices_scan_midi() {
|
||||
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 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;
|
||||
midi_identifier << ";" << "MIDI";
|
||||
midi_identifier << ";" << midi_device_id;
|
||||
midi_identifier << ";" << midi_device_caps.szPname;
|
||||
midi_identifier << ";" << midi_device_caps.wMid;
|
||||
midi_identifier << ";" << midi_device_caps.wPid;
|
||||
|
||||
// build device
|
||||
Device midi_device {};
|
||||
midi_device.id = devices.size() + 1;
|
||||
midi_device.type = MIDI;
|
||||
midi_device.handle = midi_device_handle;
|
||||
midi_device.name = midi_identifier.str();
|
||||
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;
|
||||
|
||||
// check for duplicate handle
|
||||
// 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.str()) {
|
||||
if (device.name == midi_identifier) {
|
||||
|
||||
// carry over ID
|
||||
midi_device.id = device.id;
|
||||
|
||||
// destruct and replace
|
||||
// destruct and replace, reusing the slot's existing mutexes
|
||||
this->devices_destruct(&device);
|
||||
reuse_device_mutexes(midi_device, device);
|
||||
device = midi_device;
|
||||
|
||||
// notify change
|
||||
@@ -929,9 +1092,13 @@ void rawinput::RawInputManager::devices_scan_midi() {
|
||||
cb.f(cb.data, &device);
|
||||
}
|
||||
|
||||
return;
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (replaced) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// add device to list
|
||||
auto &device = this->devices.emplace_back(midi_device);
|
||||
@@ -941,11 +1108,41 @@ void rawinput::RawInputManager::devices_scan_midi() {
|
||||
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;
|
||||
@@ -978,6 +1175,8 @@ void rawinput::RawInputManager::devices_scan_piuio() {
|
||||
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;
|
||||
@@ -1005,6 +1204,8 @@ void rawinput::RawInputManager::devices_scan_smxstage() {
|
||||
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;
|
||||
@@ -1032,6 +1233,8 @@ void rawinput::RawInputManager::devices_scan_smxdedicab() {
|
||||
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
|
||||
@@ -1072,7 +1275,11 @@ void rawinput::RawInputManager::devices_scan_xinput() {
|
||||
if (prev_device.type == DESTROYED) {
|
||||
log_info("rawinput", "overwriting previously destroyed XInput device: {}", prev_device.name);
|
||||
const auto old_id = prev_device.id;
|
||||
prev_device = create_device(player);
|
||||
|
||||
// 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
|
||||
@@ -1171,11 +1378,7 @@ void rawinput::RawInputManager::output_start() {
|
||||
// iterate all devices
|
||||
do {
|
||||
output_thread_ready = false;
|
||||
for (auto &device : this->devices) {
|
||||
|
||||
// write output
|
||||
device_write_output(&device, true);
|
||||
}
|
||||
this->devices_write_output_snapshot(true);
|
||||
} while (output_thread_ready);
|
||||
}
|
||||
});
|
||||
@@ -1336,6 +1539,8 @@ void rawinput::RawInputManager::sextet_register(const std::string &port_name, co
|
||||
|
||||
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;
|
||||
@@ -1361,6 +1566,8 @@ void rawinput::RawInputManager::sextet_register(const std::string &port_name, co
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -1370,9 +1577,15 @@ void rawinput::RawInputManager::devices_remove(const std::string &name) {
|
||||
|
||||
// remove device
|
||||
this->devices_destruct(&device);
|
||||
return;
|
||||
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() {
|
||||
@@ -1509,21 +1722,25 @@ void rawinput::RawInputManager::devices_unregister() {
|
||||
}
|
||||
|
||||
void rawinput::RawInputManager::devices_destruct() {
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(this->devices_mutex);
|
||||
|
||||
// check if there's something to destruct
|
||||
if (this->devices.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// dispose devices
|
||||
// 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) {
|
||||
@@ -1538,6 +1755,12 @@ void rawinput::RawInputManager::devices_destruct(Device *device, bool 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;
|
||||
@@ -1564,8 +1787,15 @@ void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
|
||||
}
|
||||
break;
|
||||
case MIDI:
|
||||
midiInReset((HMIDIIN) device->handle);
|
||||
midiInClose((HMIDIIN) device->handle);
|
||||
// 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();
|
||||
@@ -1589,7 +1819,10 @@ void rawinput::RawInputManager::devices_destruct(Device *device, bool log) {
|
||||
device->smxstageInfo = nullptr;
|
||||
delete device->smxdedicabInfo;
|
||||
device->smxdedicabInfo = nullptr;
|
||||
// TODO: check if mutex can be deleted
|
||||
|
||||
// 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(
|
||||
@@ -1635,6 +1868,8 @@ LRESULT CALLBACK rawinput::RawInputManager::input_wnd_proc(
|
||||
|
||||
// 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
|
||||
@@ -2045,6 +2280,9 @@ void CALLBACK rawinput::RawInputManager::input_midi_proc(HMIDIIN hMidiIn, UINT w
|
||||
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;
|
||||
@@ -2696,10 +2934,24 @@ void rawinput::RawInputManager::devices_flush_output(bool optimized) {
|
||||
}
|
||||
|
||||
// blocking routine
|
||||
for (auto &device : this->devices) {
|
||||
this->devices_write_output_snapshot(false);
|
||||
}
|
||||
|
||||
// write output
|
||||
device_write_output(&device, 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2712,6 +2964,9 @@ void rawinput::RawInputManager::devices_print() {
|
||||
|
||||
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());
|
||||
@@ -2905,6 +3160,10 @@ rawinput::Device *rawinput::RawInputManager::devices_get(const std::string &name
|
||||
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) {
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <condition_variable>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
@@ -63,10 +65,38 @@ namespace rawinput {
|
||||
class RawInputManager {
|
||||
private:
|
||||
|
||||
HotplugManager *hotplug;
|
||||
std::vector<Device> devices;
|
||||
HotplugManager *hotplug = nullptr;
|
||||
|
||||
// std::list (not std::vector) so adding/removing a device never invalidates
|
||||
// pointers to other elements. devices_get() hands out pointers that escape the
|
||||
// lock and are dereferenced later by pollers, so a concurrent scan must not
|
||||
// relocate existing devices
|
||||
std::list<Device> devices;
|
||||
std::recursive_mutex devices_mutex;
|
||||
|
||||
WNDCLASSEX input_hwnd_class {};
|
||||
std::thread *input_thread = nullptr;
|
||||
std::thread *midi_thread = nullptr;
|
||||
|
||||
// guards the MIDI scan scheduler flags below. only held around the flag
|
||||
// checks, never across the (slow) enumeration, so it makes "is a scan
|
||||
// running?" and the worker's "exit or rescan?" a single atomic decision -
|
||||
// a request can never slip into the gap between the two
|
||||
std::mutex midi_scan_m;
|
||||
|
||||
// true while a scan worker thread is running. only one runs at a time
|
||||
bool midi_scan_active = false;
|
||||
|
||||
// set when a scan is requested while one is already running, so the worker
|
||||
// rescans once instead of dropping the request (events that arrive during
|
||||
// the slow initial scan would otherwise be lost)
|
||||
bool midi_scan_pending = false;
|
||||
|
||||
// MIDI handles pending close. devices_destruct() runs under devices_mutex but
|
||||
// midiInReset/midiInClose must not (they block on the MIDI callback, which
|
||||
// also takes devices_mutex), so handles are queued here and closed by
|
||||
// midi_close_deferred_flush() once the lock is released
|
||||
std::vector<HMIDIIN> midi_close_deferred;
|
||||
std::thread *flush_thread = nullptr;
|
||||
bool flush_thread_running = false;
|
||||
std::mutex flush_thread_m;
|
||||
@@ -83,6 +113,8 @@ namespace rawinput {
|
||||
void input_hwnd_create();
|
||||
void input_hwnd_destroy();
|
||||
void devices_reload();
|
||||
void midi_scan_join();
|
||||
void midi_close_deferred_flush();
|
||||
void devices_scan_rawinput(RAWINPUTDEVICELIST *device, bool log = true);
|
||||
void devices_scan_piuio();
|
||||
void devices_scan_smxstage();
|
||||
@@ -93,6 +125,7 @@ namespace rawinput {
|
||||
void flush_stop();
|
||||
void output_start();
|
||||
void output_stop();
|
||||
void devices_write_output_snapshot(bool only_updated);
|
||||
|
||||
static std::string rawinput_get_device_name(HANDLE hDevice);
|
||||
static std::string rawinput_get_device_description(const DeviceInfo& info, const std::string &device_name);
|
||||
@@ -118,6 +151,7 @@ namespace rawinput {
|
||||
|
||||
void devices_scan_rawinput(const std::string &device_name = "");
|
||||
void devices_scan_midi();
|
||||
void midi_scan_start();
|
||||
void devices_scan_xinput();
|
||||
void devices_remove(const std::string &name);
|
||||
|
||||
@@ -130,11 +164,12 @@ namespace rawinput {
|
||||
void __stdcall devices_print();
|
||||
Device *devices_get(const std::string &name, bool updated = false);
|
||||
|
||||
inline std::vector<Device> &devices_get() {
|
||||
inline std::list<Device> &devices_get() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
inline std::vector<Device *> devices_get_updated() {
|
||||
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
|
||||
std::vector<Device *> updated;
|
||||
for (auto &device : devices_get()) {
|
||||
device.mutex->lock();
|
||||
@@ -150,6 +185,7 @@ namespace rawinput {
|
||||
}
|
||||
|
||||
inline void devices_midi_freeze(bool freeze) {
|
||||
std::lock_guard<std::recursive_mutex> lock(devices_mutex);
|
||||
for (auto &device : devices_get()) {
|
||||
if (device.type == MIDI) {
|
||||
device.midiInfo->freeze = freeze;
|
||||
|
||||
Reference in New Issue
Block a user