Skip to content

Add hid_set_num_input_buffers() API#787

Open
auxcorelabs wants to merge 4 commits intolibusb:masterfrom
auxcorelabs:feature/input-report-buffer-size
Open

Add hid_set_num_input_buffers() API#787
auxcorelabs wants to merge 4 commits intolibusb:masterfrom
auxcorelabs:feature/input-report-buffer-size

Conversation

@auxcorelabs
Copy link
Copy Markdown

@auxcorelabs auxcorelabs commented Apr 17, 2026

Exposes a new public function hid_set_num_input_buffers(dev, num_buffers) to resize the per-device input report queue.

High-throughput HID devices (medical telemetry, high-poll-rate gaming peripherals, data acquisition hardware) can emit bursts of input reports that exceed the current hardcoded queue sizes (30 on macOS and Linux/libusb, 64 on Windows). When a burst exceeds the queue, reports are silently dropped with no error indication to the caller. Observed on hardware emitting ~90 reports in 200 ms bursts: both the macOS and Linux/libusb backends silently drop ~20% of frames.

Per-backend behavior

  • macOS: resizes the userspace input-report queue
  • Linux libusb: resizes the userspace report queue
  • Windows: wraps HidD_SetNumInputBuffers (parity with existing behavior)
  • Linux hidraw / NetBSD: no-op (kernel manages buffering — documented as a @note on the API declaration in hidapi.h)

Safety

  • Values outside [1, HID_API_MAX_NUM_INPUT_BUFFERS] return -1 and register a descriptive error on the device (via the backend's register_device_error helper where available)
  • Thread-safe: setter uses the same lock (dev->mutex / dev->thread_state) as the respective report callback

Design notes

  • Backwards compatibility: Purely additive public API. Defaults per backend are unchanged, so existing callers see no behavioral change. hid_device is opaque in hidapi.h, so struct field additions in backend-private files are not an ABI break.
  • Why 1024 cap: Bounds per-device memory to roughly 1 MB on USB high-speed links, preventing memory exhaustion from a caller that passes INT_MAX (malicious or buggy). We've measured bursts up to ~90 reports deep in practice; 1024 leaves room for devices ~10× more demanding. Overridable at build time via -DHID_API_MAX_NUM_INPUT_BUFFERS=N for memory-constrained targets.
  • No getter: hidraw and NetBSD back the queue in the kernel with sizes not portably exposed to userspace, so a cross-platform getter couldn't return a meaningful value there.
  • Why not hid_get_input_report() instead? That API issues a host-initiated GET_REPORT request (via the control endpoint or OS equivalent). It does not drain the interrupt-endpoint queue that streaming devices fill, and in practice many devices either don't implement GET_REPORT for input reports or respond incorrectly. The two APIs use different USB transfer mechanisms (interrupt endpoint vs. control endpoint); this PR fixes the interrupt-streaming path.

Userspace queue: linked list → ring buffer

The latest commit replaces the linked list with a fixed-size ring:

  • O(1) enqueue/dequeue. hid_report_callback / read_callback no longer scan a tail on every incoming report.
  • Exact cap semantics. Setter calls hidapi_input_ring_drop_oldest on shrink, so dev->num_input_buffers is the precise steady-state queue length (matches Copilot's "limit corresponds precisely to num_input_buffers" request).
  • Clean allocation-failure handling. The previous code had an unchecked allocation that would segfault on OOM. The new push returns -1 cleanly; libusb drops the report silently (no active error channel), mac calls register_device_error.
  • ABI unchanged. hid_device is opaque; the struct field swap is invisible to callers.
  • Implementation. hidapi_input_ring_* static-in-header helper, present as byte-identical copies in libusb/ and mac/.

References

Exposes a new public function to resize the per-device input report
queue. High-throughput HID devices (medical telemetry, high-poll-rate
gaming peripherals, data acquisition hardware) emit bursts of input
reports that exceed the current hardcoded queue sizes (30 on macOS and
the libusb backend, 64 on Windows). When a burst exceeds the queue,
reports are silently dropped with no error indication to the caller.

This adds:
- hid_set_input_report_buffer_size(dev, size) in hidapi.h
- HID_API_MAX_INPUT_REPORT_BUFFER_SIZE (1024) cap to prevent unbounded
  memory growth

Per-backend behavior:
- macOS: resizes the userspace IOHIDQueue-fed report queue
- Linux libusb: resizes the userspace report queue
- Windows: wraps HidD_SetNumInputBuffers (parity with existing behavior)
- Linux hidraw: no-op (kernel manages buffering)
- NetBSD: no-op (kernel manages buffering)

Defaults are unchanged, so existing callers are unaffected. Values
outside [1, HID_API_MAX_INPUT_REPORT_BUFFER_SIZE] are rejected with -1.
Thread-safe on macOS (dev->mutex) and libusb (dev->thread_state),
matching the locks used by the respective report callbacks.

Addresses the same need as closed issue libusb#154 (HidD_SetNumInputBuffers
exposure) and complements libusb#725 (callback-based input API).
@mcuee mcuee added API API change, Version 1 stuff enhancement New feature or request labels Apr 17, 2026
Comment thread linux/hid.c Outdated
Comment thread linux/hid.c Outdated
Comment thread mac/hid.c Outdated
Comment thread netbsd/hid.c Outdated
Comment thread windows/hid.c Outdated
Copy link
Copy Markdown
Member

@Youw Youw left a comment

Choose a reason for hiding this comment

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

refer to comments, otherwise looks good

Per maintainer feedback on PR libusb#787:

- Remove if (!dev) validation from all 5 backends. hidapi convention is that device functions trust the caller to pass a valid handle; only hid_close is permitted to accept NULL.

- Reword the inline comment in linux/hid.c and netbsd/hid.c to lead with "No-op" so the caller-visible behavior is explicit at the implementation site.
@auxcorelabs
Copy link
Copy Markdown
Author

Thanks for reviewing. I have made the changes.

@JoergAtGithub
Copy link
Copy Markdown
Contributor

I think the name of the Win32-API HidD_SetNumInputBuffers describes better what it does than hid_set_input_report_buffer_size, as it does change the number of buffers and not the size of the bufferin bytes.

… controls the number of input report buffers, not their byte size.

- Function:    hid_set_input_report_buffer_size -> hid_set_num_input_buffers
- Macro:       HID_API_MAX_INPUT_REPORT_BUFFER_SIZE -> HID_API_MAX_NUM_INPUT_BUFFERS
- Parameter:   buffer_size -> num_buffers
- Error string: "buffer_size out of range" -> "num_buffers out of range"
@auxcorelabs auxcorelabs changed the title Add hid_set_input_report_buffer_size() API Add hid_set_num_input_buffers() API Apr 19, 2026
@auxcorelabs
Copy link
Copy Markdown
Author

Good callout @JoergAtGithub. Have renamed the function to hid_set_num_input_buffers() and made the associated changes.

@Youw - Please let me know if any further changes required.

Thanks both.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Apr 19, 2026

@auxcorelabs
Just wondering if you can enhance the hidtest application to include this new API.

If not, do you have a simple test to share? Thanks.

Comment thread linux/hid.c Outdated
…ile commentary, add hidtest coverage

- hidapi/hidapi.h: replace the Defaults per backend list with an
  @note Per-backend behavior block covering macOS / Windows /
  libusb / hidraw / uhid semantics, ranges, and defaults. Per
  @Youw, the public header is the canonical place for the
  cross-backend contract.
- linux/hid.c, netbsd/hid.c: drop the comment that cross-referenced
  other backends. The (void)num_buffers; idiom and the header
  contract speak for themselves.
- libusb/hid.c: drop the self-scoped no-error-registration note
  for the same reason.
- hidtest/test.c: add a compile-time symbol reference and a
  runtime call hid_set_num_input_buffers(handle, 500) right after
  hid_open() succeeds, per @mcuee. Both guarded on
  HID_API_VERSION >= 0.16.0 so they activate in the 0.16 release
  cycle, matching the precedent of hid_send_output_report at
  0.15.0.
Copy link
Copy Markdown
Member

@Youw Youw left a comment

Choose a reason for hiding this comment

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

So far so good.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new public HIDAPI entry point to let callers increase the per-device input report queue depth, addressing silent report drops during bursty/high-throughput input on backends that queue reports in userspace (macOS + Linux/libusb) and exposing the existing Windows kernel buffering control.

Changes:

  • Introduces hid_set_num_input_buffers(dev, num_buffers) and documents it in hidapi.h (with a max-cap macro).
  • Updates macOS and Linux/libusb backends to enforce queue limits via a per-device num_input_buffers value (default 30).
  • Adds no-op stub implementations for Linux hidraw and NetBSD uhid; updates hidtest to exercise the new API when available.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
hidapi/hidapi.h Declares and documents the new public API and its max-cap macro.
mac/hid.c Adds per-device buffer cap field, uses it in the report callback, and provides a setter guarded by the device mutex.
libusb/hid.c Adds per-device buffer cap field, uses it in the read callback, and provides a setter guarded by the thread-state mutex.
windows/hid.c Implements the API by forwarding to HidD_SetNumInputBuffers.
linux/hid.c Adds a validated no-op stub for the hidraw backend.
netbsd/hid.c Adds a validated no-op stub for the uhid backend.
hidtest/test.c Calls the new API under a version guard to ensure all backends export it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread mac/hid.c
Comment on lines 910 to 915
/* Pop one off if we've reached 30 in the queue. This
way we don't grow forever if the user never reads
anything from the device. */
if (num_queued > 30) {
if (num_queued > dev->num_input_buffers) {
return_data(dev, NULL, 0);
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

The queue-length enforcement here becomes both expensive and semantically fuzzy now that callers can raise num_input_buffers up to 1024. Each report traverses the entire linked list to find the tail (O(n)), which can become a hot path at high report rates, and the current num_queued accounting means the steady-state queue length is not exactly dev->num_input_buffers. Consider tracking a tail pointer and an explicit queued-count (or switching to a ring buffer) so enqueue/drop is O(1) and the limit corresponds precisely to num_input_buffers.

Copilot uses AI. Check for mistakes.
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.

Someone needs to double-check this, but I'm pretty sure Copilot is right here.
We might even need to consider one step further and switch from linked list to a ring buffer as an optimisation

Comment thread libusb/hid.c
Comment on lines 989 to 994
/* Pop one off if we've reached 30 in the queue. This
way we don't grow forever if the user never reads
anything from the device. */
if (num_queued > 30) {
if (num_queued > dev->num_input_buffers) {
return_data(dev, NULL, 0);
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Same concern as the macOS backend: with num_input_buffers now configurable up to 1024, this per-report linked-list tail scan is O(n) and can become a significant CPU cost during bursts. Also, the current num_queued counting makes the effective cap differ from dev->num_input_buffers. Consider keeping a tail pointer + queued-count (or a ring buffer) so enqueue/drop is O(1) and the limit matches the API’s meaning.

Copilot uses AI. Check for mistakes.
Comment thread hidapi/hidapi.h
Comment on lines +431 to +432
*/
#define HID_API_MAX_NUM_INPUT_BUFFERS 1024
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

HID_API_MAX_NUM_INPUT_BUFFERS is defined unconditionally, which makes the “overridable via -D…” behavior described in the PR text impossible (command-line defines will be overwritten by this header definition). If the intent is to allow build-time overrides, wrap this in #ifndef HID_API_MAX_NUM_INPUT_BUFFERS (or similar) so downstreams can set the cap without patching the header.

Suggested change
*/
#define HID_API_MAX_NUM_INPUT_BUFFERS 1024
*/
#ifndef HID_API_MAX_NUM_INPUT_BUFFERS
#define HID_API_MAX_NUM_INPUT_BUFFERS 1024
#endif

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@Youw Youw Apr 21, 2026

Choose a reason for hiding this comment

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

true - either a PR descriptino or code needs to be updated, I don't really have a preference

Comment thread hidapi/hidapi.h
Comment on lines +434 to +468
/** @brief Set the number of input report buffers queued per device.

Some HID devices emit input reports in bursts at rates
that exceed the default internal queue capacity, causing
silent report drops on macOS and the libusb Linux backend.
This function allows callers to change how many input
report buffers are retained per device.

Call after hid_open() and before the first hid_read() to
avoid losing reports buffered at open time.

@note Per-backend behavior:
- **macOS (IOKit)** and **Linux libusb**: resizes the
userspace input-report queue. Default: 30 reports.
- **Windows**: forwards to HidD_SetNumInputBuffers(),
which resizes the kernel HID ring buffer. The kernel
accepts values in the range [2, 512]; requests outside
this range return -1. Default: 64 reports.
- **Linux hidraw** and **NetBSD uhid**: the call is
accepted (returns 0) and validated against
HID_API_MAX_NUM_INPUT_BUFFERS, but has no effect (no-op) —
these kernels manage the input report buffer internally
and expose no userspace resize.

@ingroup API
@param dev A device handle returned from hid_open().
@param num_buffers The desired number of input report buffers.
Must be in range [1, HID_API_MAX_NUM_INPUT_BUFFERS].

@returns
0 on success, -1 on failure (invalid parameters or
backend-specific error). Call hid_error(dev) for
details where supported.
*/
int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers);
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

PR description mentions a new public API named hid_set_input_report_buffer_size(dev, size), but the code adds hid_set_num_input_buffers(dev, num_buffers) instead. Please either update the PR description to match the shipped API name, or rename the API in code (including docs) so they are consistent.

Copilot uses AI. Check for mistakes.
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.

minor: need update PR description

Comment thread linux/hid.c
int HID_API_EXPORT hid_set_num_input_buffers(hid_device *dev, int num_buffers)
{
if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) {
register_error_str(&dev->last_error_str, "num_buffers out of range");
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

This backend has a register_device_error() helper for setting dev->last_error_str, but this new API writes dev->last_error_str via register_error_str() directly. For consistency (and to keep any future behavior in the helper centralized), consider using register_device_error(dev, "num_buffers out of range") here.

Suggested change
register_error_str(&dev->last_error_str, "num_buffers out of range");
register_device_error(dev, "num_buffers out of range");

Copilot uses AI. Check for mistakes.
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.

agree

Comment thread netbsd/hid.c
int HID_API_EXPORT HID_API_CALL hid_set_num_input_buffers(hid_device *dev, int num_buffers)
{
if (num_buffers <= 0 || num_buffers > HID_API_MAX_NUM_INPUT_BUFFERS) {
register_error_str(&dev->last_error_str, "num_buffers out of range");
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Similar to other NetBSD APIs, prefer using the register_device_error() helper (or the same error helper used elsewhere in this backend) rather than writing dev->last_error_str via register_error_str() directly. This keeps error-setting behavior consistent across the file.

Suggested change
register_error_str(&dev->last_error_str, "num_buffers out of range");
register_device_error(dev, "num_buffers out of range");

Copilot uses AI. Check for mistakes.
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.

agree

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API API change, Version 1 stuff enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants