Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
33 changes: 26 additions & 7 deletions src/controllers/hid/hidcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,33 +70,52 @@ class HidControllerJSProxy : public ControllerJSProxy {
/// @brief Sends HID OutputReport to HID device
/// @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 reportID 1...255 for HID devices that uses ReportIDs - or 0 for
/// devices, which don't use ReportIDs
/// @param useNonSkippingFIFO (optional)
Comment thread
JoergAtGithub marked this conversation as resolved.
/// Same as argument useNonSkippingFIFO of the sendOutputReport function,
/// which is documented below
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
94 changes: 94 additions & 0 deletions src/controllers/hid/hidioglobaloutputreportfifo.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#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 size_t kMaxHidErrorMessageSize = 512;
constexpr size_t kSizeOfFifoInReports = 32;
} // namespace

HidIoGlobalOutputReportFifo::HidIoGlobalOutputReportFifo()
: m_fifoQueue(kSizeOfFifoInReports) {
Copy link
Copy Markdown
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
Copy Markdown
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(reportData);
report.prepend(reportId); // In Qt6 this is a very fast operation without reallocation
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why? Prepend is implemented as insert()
https://github.com/qt/qtbase/blob/aaaa1e251ea43e4094e79b7161327c07db60d351/src/corelib/text/qbytearray.cpp#L898
I think by luck there are some reserve bytes. But this call also performs a deep copy of the implicit shared.
They might be some reserve bytes that avoid allocation. They should be also there with Qt5.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The Qt6 documentation explicitly say, that this is very fast: https://doc.qt.io/qt-6/qbytearray.html#prepend


// 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();

QByteArray* pFront = m_fifoQueue.front();

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;
}

// Array containing the ReportID byte followed by the data to be send
QByteArray reportToSend(std::move(*pFront));
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*>(reportToSend.constData()),
reportToSend.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>(reportToSend[0])
<< ") sent from non-skipping FIFO - 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;
};
51 changes: 27 additions & 24 deletions src/controllers/hid/hidiooutputreport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace {
constexpr int kReportIdSize = 1;
constexpr int kMaxHidErrorMessageSize = 512;
constexpr size_t kMaxHidErrorMessageSize = 512;
} // namespace

HidIoOutputReport::HidIoOutputReport(
Expand All @@ -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) {
Comment thread
JoergAtGithub marked this conversation as 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
Loading