Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIFO mode option for HID OutputReports #11326

Merged
merged 15 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2935,6 +2935,7 @@ if(HID)
target_sources(mixxx-lib PRIVATE
src/controllers/hid/hidcontroller.cpp
src/controllers/hid/hidiothread.cpp
src/controllers/hid/hidioglobaloutputreportfifo.cpp
src/controllers/hid/hidiooutputreport.cpp
src/controllers/hid/hiddevice.cpp
src/controllers/hid/hidenumerator.cpp
Expand Down
44 changes: 38 additions & 6 deletions src/controllers/hid/hidcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,32 +71,64 @@ class HidControllerJSProxy : public ControllerJSProxy {
/// @param dataList Data to send as list of bytes
/// @param length Unused but mandatory argument
/// @param reportID 1...255 for HID devices that uses ReportIDs - or 0 for devices, which don't use ReportIDs
/// @param resendUnchangedReport If set, the report will also be send, if the data are unchanged since last sending
/// @param useNonSkippingFIFO (optional)
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved
/// - False (default):
/// - Reports with identical data will be sent only once.
/// - If reports were superseded by newer data before they could be sent,
/// the oudated data will be skipped.
/// - This mode works for all USB HID class compatible reports,
/// in these each field represents the state of a control (e.g. an LED).
/// - This mode works best in overload situations, where more reports
/// are to be sent, than can be processed.
/// - True:
/// - The report will not be skipped under any circumstances,
/// except FIFO memory overflow.
/// - All reports with useNonSkippingFIFO set True will be send before
/// any cached report with useNonSkippingFIFO set False.
/// - All reports with useNonSkippingFIFO set True will be send in
/// strict First In / First Out (FIFO) order.
/// - Limit the use of this mode to the places, where it is really necessary.
Q_INVOKABLE void send(const QList<int>& dataList,
unsigned int length,
quint8 reportID,
bool resendUnchangedReport = false) {
bool useNonSkippingFIFO = false) {
Q_UNUSED(length);
QByteArray dataArray;
dataArray.reserve(dataList.size());
for (int datum : dataList) {
dataArray.append(datum);
}
sendOutputReport(reportID, dataArray, resendUnchangedReport);
sendOutputReport(reportID, dataArray, useNonSkippingFIFO);
}

/// @brief Sends an OutputReport to HID device
/// @param reportID 1...255 for HID devices that uses ReportIDs - or 0 for devices, which don't use ReportIDs
/// @param dataArray Data to send as byte array (Javascript type Uint8Array)
/// @param resendUnchangedReport If set, the report will also be send, if the data are unchanged since last sending
/// @param useNonSkippingFIFO (optional)
/// - False (default):
/// - Reports with identical data will be sent only once.
/// - If reports were superseded by newer data before they could be sent,
/// the oudated data will be skipped.
/// - This mode works for all USB HID class compatible reports,
/// in these each field represents the state of a control (e.g. an LED).
/// - This mode works best in overload situations, where more reports
/// are to be sent, than can be processed.
/// - True:
/// - The report will not be skipped under any circumstances,
/// except FIFO memory overflow.
/// - All reports with useNonSkippingFIFO set True will be send before
/// any cached report with useNonSkippingFIFO set False.
/// - All reports with useNonSkippingFIFO set True will be send in
/// strict First In / First Out (FIFO) order.
/// - Limit the use of this mode to the places, where it is really necessary.
Q_INVOKABLE void sendOutputReport(quint8 reportID,
const QByteArray& dataArray,
bool resendUnchangedReport = false) {
bool useNonSkippingFIFO = false) {
VERIFY_OR_DEBUG_ASSERT(m_pHidController->m_pHidIoThread) {
return;
}
m_pHidController->m_pHidIoThread->updateCachedOutputReportData(
reportID, dataArray, resendUnchangedReport);
reportID, dataArray, useNonSkippingFIFO);
}

/// @brief getInputReport receives an InputReport from the HID device on request.
Expand Down
97 changes: 97 additions & 0 deletions src/controllers/hid/hidioglobaloutputreportfifo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#include "controllers/hid/hidioglobaloutputreportfifo.h"

#include <hidapi.h>

#include "controllers/defs_controllers.h"
#include "controllers/hid/legacyhidcontrollermappingfilehandler.h"
#include "util/compatibility/qbytearray.h"
#include "util/compatibility/qmutex.h"
#include "util/string.h"
#include "util/time.h"
#include "util/trace.h"

namespace {
constexpr int kMaxHidErrorMessageSize = 512;
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved
constexpr int kSizeOfFifoInReports = 32;
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved
} // namespace

HidIoGlobalOutputReportFifo::HidIoGlobalOutputReportFifo()
: m_fifoQueue(kSizeOfFifoInReports) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a naming missmatch beween InReports in the constant and OutputReport in the class name?
Maybe we can avoid input and output which can change form the perspective of different classes and use Received and Transmit or something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a missunderstanding. In is not a abreviation for Input here, its just the word in. The whole code is only for OutputReports.
InReports is the counted unit, like InMillis or InSeconds for times.

}

void HidIoGlobalOutputReportFifo::addReportDatasetToFifo(const quint8 reportId,
const QByteArray& reportData,
const mixxx::hid::DeviceInfo& deviceInfo,
const RuntimeLoggingCategory& logOutput) {
// First byte must always be the ReportID-Byte
QByteArray report(std::move(reportData));
report.prepend(reportId);
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved

// Swap report to lockless FIFO queue
bool success = m_fifoQueue.try_emplace(std::move(report));

// Handle the case, that the FIFO queue is full - which is an error case
if (!success) {
// If the FIFO is full, we skip the report dataset even
// in non-skipping mode, to keep the controller mapping thread
// responsive for InputReports from the controller.
// Alternative would be to block processing of the controller
// mapping thread, until the FIFO has space again.
qCWarning(logOutput)
<< "FIFO overflow: Unable to add OutputReport " << reportId
<< "to the global cache for non-skipping sending of OututReports for"
<< deviceInfo.formatName();
}
}

bool HidIoGlobalOutputReportFifo::sendNextReportDataset(QMutex* pHidDeviceAndPollMutex,
hid_device* pHidDevice,
const mixxx::hid::DeviceInfo& deviceInfo,
const RuntimeLoggingCategory& logOutput) {
auto startOfHidWrite = mixxx::Time::elapsed();

auto pFront = m_fifoQueue.front();
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved

if (pFront == nullptr) {
// No data in FIFO to be send
// Return with false, to signal the caller, that no time consuming IO
// operation was ncessary
return false;
}

auto fifoUsedEntries = m_fifoQueue.size();
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved

QByteArray dataToSend(std::move(*pFront));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dataToSend has the report ID prepended. This is not clear initially.
Can we improve the situation by always putting the report ID at the beginning?
This will also solve the reallocation of the buffer when prepending the report ID before putting it into the buffer.

The same benefit goes for
void HidController::send(QByteArray data, unsigned int reportID)
The we just not store the modified buffer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our Javascript API has seperated arguments for ReportID and report data. This is not only needed for backwards compatibility, but is also the cleanest way to define such an API.
I will improve naming and add a comment, to make clear that this variable contains ReportID + data.

m_fifoQueue.pop();

auto hidDeviceLock = lockMutex(pHidDeviceAndPollMutex);

// hid_write can take several milliseconds, because hidapi synchronizes
// the asyncron HID communication from the OS
int result = hid_write(pHidDevice,
reinterpret_cast<const unsigned char*>(dataToSend.constData()),
dataToSend.size());
if (result == -1) {
qCWarning(logOutput) << "Unable to send data to" << deviceInfo.formatName() << ":"
<< mixxx::convertWCStringToQString(
hid_error(pHidDevice),
kMaxHidErrorMessageSize);
}

hidDeviceLock.unlock();

if (result != -1) {
qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit()
<< " " << result << "bytes (including ReportID of"
<< static_cast<quint8>(dataToSend[0])
<< ") sent from non-skipping FIFO ("
<< fifoUsedEntries << "/" << kSizeOfFifoInReports
<< "used) - Needed: "
<< (mixxx::Time::elapsed() - startOfHidWrite)
.formatMicrosWithUnit();
}

// Return with true, to signal the caller, that the time consuming hid_write
// operation was executed
return true;
}
31 changes: 31 additions & 0 deletions src/controllers/hid/hidioglobaloutputreportfifo.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once

#include "controllers/controller.h"
#include "controllers/hid/hiddevice.h"
#include "rigtorp/SPSCQueue.h"
#include "util/duration.h"

/// Stores and sends OutputReports (independent of the ReportID) in First In /
/// First Out (FIFO) order
class HidIoGlobalOutputReportFifo {
public:
HidIoGlobalOutputReportFifo();

/// Caches new OutputReport to the FIFO, which will later be send by the IO thread
void addReportDatasetToFifo(const quint8 reportId,
const QByteArray& reportData,
const mixxx::hid::DeviceInfo& deviceInfo,
const RuntimeLoggingCategory& logOutput);

/// Sends the next OutputReport from FIFO to the HID device,
/// when if any report is cached in FIFO.
/// Returns true if a time consuming hid_write operation was executed.
bool sendNextReportDataset(QMutex* pHidDeviceAndPollMutex,
hid_device* pHidDevice,
const mixxx::hid::DeviceInfo& deviceInfo,
const RuntimeLoggingCategory& logOutput);

private:
// Lockless FIFO queue
rigtorp::SPSCQueue<QByteArray> m_fifoQueue;
};
49 changes: 26 additions & 23 deletions src/controllers/hid/hidiooutputreport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,41 @@ HidIoOutputReport::HidIoOutputReport(
void HidIoOutputReport::updateCachedData(const QByteArray& data,
const mixxx::hid::DeviceInfo& deviceInfo,
const RuntimeLoggingCategory& logOutput,
bool resendUnchangedReport) {
HidIoGlobalOutputReportFifo* pGlobalOutputReportFifo,
bool useNonSkippingFIFO) {
auto cacheLock = lockMutex(&m_cachedDataMutex);

if (!m_lastCachedDataSize) {
// First call updateCachedData for this report
m_lastCachedDataSize = data.size();

} else {
if (m_possiblyUnsentDataCached) {
if (m_possiblyUnsentDataCached && !useNonSkippingFIFO) {
JoergAtGithub marked this conversation as resolved.
Show resolved Hide resolved
qCDebug(logOutput) << "t:" << mixxx::Time::elapsed().formatMillisWithUnit()
<< "Skipped superseded OutputReport"
<< deviceInfo.formatName() << "serial #"
<< deviceInfo.serialNumber() << "(Report ID"
<< m_reportId << ")";
<< "skipped superseded OutputReport data for ReportID"
<< m_reportId;
}

// The size of an HID report is defined in a HID device and can't vary at runtime
if (m_lastCachedDataSize != data.size()) {
qCWarning(logOutput) << "Size of report (with Report ID"
<< m_reportId << ") changed from"
<< m_lastCachedDataSize
<< "to" << data.size() << "for"
<< deviceInfo.formatName() << "serial #"
<< deviceInfo.serialNumber()
<< "- This indicates a bug in the mapping code!";
qCWarning(logOutput)
<< "Size of OutputReport ( with ReportID" << m_reportId
<< ") changed from" << m_lastCachedDataSize << "to"
<< data.size()
<< "- This indicates a bug in the mapping code!";
m_lastCachedDataSize = data.size();
}
}

// m_possiblyUnsentDataCached must be set while m_cachedDataMutex is locked
// This step covers the case that data for the report are cached in skipping mode,
// succeed by a non-skipping send of the same report
if (useNonSkippingFIFO) {
m_possiblyUnsentDataCached = false;
m_lastSentData.clear();
return;
}

// Deep copy with reusing the already allocated heap memory
// The first byte with the ReportID is not overwritten
qByteArrayReplaceWithPositionAndSize(&m_cachedData,
Expand All @@ -66,7 +72,6 @@ void HidIoOutputReport::updateCachedData(const QByteArray& data,
data.constData(),
data.size());
m_possiblyUnsentDataCached = true;
m_resendUnchangedReport = resendUnchangedReport;
}

bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex,
Expand All @@ -82,7 +87,7 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex,
return false;
}

if (!(m_resendUnchangedReport || m_lastSentData.compare(m_cachedData))) {
if (!m_lastSentData.compare(m_cachedData)) {
// An HID OutputReport can contain only HID OutputItems.
// HID OutputItems are defined to represent the state of one or more similar controls or LEDs.
// Only HID Feature items may be attributes of other items.
Expand All @@ -96,10 +101,8 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex,
cacheLock.unlock();

qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit()
<< " Skipped identical Output Report for"
<< deviceInfo.formatName() << "serial #"
<< deviceInfo.serialNumber() << "(Report ID"
<< m_reportId << ")";
<< "Skipped sending identical OutputReport data from cache for ReportID"
<< m_reportId;

// Return with false, to signal the caller, that no time consuming IO operation was necessary
return false;
Expand All @@ -123,7 +126,7 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex,
reinterpret_cast<const unsigned char*>(m_lastSentData.constData()),
m_lastSentData.size());
if (result == -1) {
qCWarning(logOutput) << "Unable to send data to" << deviceInfo.formatName() << ":"
qCWarning(logOutput) << "Unable to send data to device :"
<< mixxx::convertWCStringToQString(
hid_error(pHidDevice),
kMaxHidErrorMessageSize);
Expand All @@ -147,9 +150,9 @@ bool HidIoOutputReport::sendCachedData(QMutex* pHidDeviceAndPollMutex,
}

qCDebug(logOutput) << "t:" << startOfHidWrite.formatMillisWithUnit() << " "
<< result << "bytes sent to" << deviceInfo.formatName()
<< "serial #" << deviceInfo.serialNumber()
<< "(including report ID of" << m_reportId << ") - Needed: "
<< result << "bytes ( including ReportID of"
<< static_cast<quint8>(m_reportId)
<< ") sent from skipping cache - Needed:"
<< (mixxx::Time::elapsed() - startOfHidWrite).formatMicrosWithUnit();

// Return with true, to signal the caller, that the time consuming hid_write operation was executed
Expand Down
8 changes: 4 additions & 4 deletions src/controllers/hid/hidiooutputreport.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "controllers/controller.h"
#include "controllers/hid/hiddevice.h"
#include "controllers/hid/hidioglobaloutputreportfifo.h"
#include "util/compatibility/qmutex.h"
#include "util/duration.h"

Expand All @@ -11,10 +12,10 @@ class HidIoOutputReport {

/// Caches new report data, which will later send by the IO thread
void updateCachedData(const QByteArray& data,

const mixxx::hid::DeviceInfo& deviceInfo,
const RuntimeLoggingCategory& logOutput,
bool resendUnchangedReport);
HidIoGlobalOutputReportFifo* pGlobalOutputReportFifo,
bool useNonSkippingFIFO);

/// Sends the OutputReport to the HID device, when changed data are cached.
/// Returns true if a time consuming hid_write operation was executed.
Expand All @@ -28,12 +29,11 @@ class HidIoOutputReport {
QByteArray m_lastSentData;

/// Mutex must be locked when reading/writing m_cachedData
/// or m_possiblyUnsentDataCached, m_resendUnchangedReport
/// or m_possiblyUnsentDataCached
QMutex m_cachedDataMutex;

QByteArray m_cachedData;
bool m_possiblyUnsentDataCached;
bool m_resendUnchangedReport;

/// Due to swapping of the QbyteArrays, we need to store
/// this information independent of the QBytearray size
Expand Down
24 changes: 22 additions & 2 deletions src/controllers/hid/hidiothread.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ HidIoThread::HidIoThread(
m_pHidDevice(pHidDevice),
m_lastPollSize(0),
m_pollingBufferIndex(0),
m_globalOutputReportFifo(),
m_runLoopSemaphore(1) {
// Initializing isn't strictly necessary but is good practice.
for (int i = 0; i < kNumBuffers; i++) {
Expand Down Expand Up @@ -188,7 +189,7 @@ QByteArray HidIoThread::getInputReport(quint8 reportID) {

void HidIoThread::updateCachedOutputReportData(quint8 reportID,
const QByteArray& data,
bool resendUnchangedReport) {
bool useNonSkippingFIFO) {
auto mapLock = lockMutex(&m_outputReportMapMutex);
if (m_outputReports.find(reportID) == m_outputReports.end()) {
std::unique_ptr<HidIoOutputReport> pNewOutputReport;
Expand All @@ -204,11 +205,30 @@ void HidIoThread::updateCachedOutputReportData(quint8 reportID,

mapLock.unlock();

// If useNonSkippingFIFO is false, the report data are cached here
// If useNonSkippingFIFO is true, this cache is cleared
actualOutputReportIterator->second->updateCachedData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the mutex locked and this operation invoked even when using the FIFO strategy? The cache and the mutex should be bypassed. Ideally the dispatch strategy for each reportID would be immutable and configured at construction time. At least the FIFO report IDs need to be known in advance to avoid the locking.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call of updateCachedData is always needed to guarantee seamless transitions between both modes.
If a skipable report is cached, it must be cleared, because otherwise it would be send after the report from the non-skipable report from the FIFO - and would overwrite with outdated data. This happens in updateCachedData as well as the check for errors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I acknowledge that the current design is not suitable to for this optimization. But in principle the dispatch strategy should be fixed per report ID and not change with every call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caching the mode only for the sender would avoid locking the mutex with every call. The cache only needs to be cleared once after changing the mode.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only datastructure per output report is currently HidIoOutputReport. Adding a additional data structure per output report would prevent the mutex locking in FIFO mode. But this mutex is only locked for the very short time of looking up the map for the map iterator of the report.
This takes nanoseconds, while the sending of a report takes several milliseconds. And in FIFO mode, calling this function must not be done in higher rate than the HID subsystem can send reports to the device. This ratio makes a mutex blocking very unlikely - and even when it happens, it will block the other thread only for several nanoseconds.
I don't think, that it makes sense to add additional code here, because the impact will be insignificant in practice.

Copy link
Contributor

@uklotzde uklotzde Apr 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However short and tiny the critical section might be, you have no control over the OS scheduler -> dependency inversion. Generous use of mutexes is always a red herring. They are very often used for compensating architectural deficiencies.

https://twitter.com/EshlemanMatthew/status/1641073049746251782

data, m_deviceInfo, m_logOutput, resendUnchangedReport);
data, m_deviceInfo, m_logOutput, &m_globalOutputReportFifo, useNonSkippingFIFO);

// If useNonSkippingFIFO is true, put the new report dataset on the FIFO
if (useNonSkippingFIFO) {
m_globalOutputReportFifo.addReportDatasetToFifo(reportID, data, m_deviceInfo, m_logOutput);
}
}

bool HidIoThread::sendNextCachedOutputReport() {
// 1.) Send non-skipping reports from FIFO
if (m_globalOutputReportFifo.sendNextReportDataset(&m_hidDeviceAndPollMutex,
m_pHidDevice,
m_deviceInfo,
m_logOutput)) {
// Return after each time consuming sendCachedData
return true;
}

// 2.) If non non-skipping reports were in the FIFO, send the skipable reports
// from the m_outputReports cache

// m_outputReports.size() doesn't need mutex protection, because the value of i is not used.
// i is just a counter to prevent infinite loop execution.
// If the map size increases, this loop will execute one iteration more,
Expand Down
Loading