Skip to content

feat(libSceUsbd): handle events with FreeBSD libusb's event lock and handler thread - #9339

Merged
oneandonlydean merged 3 commits into
boykopovar:mainfrom
MaxLikesCode:feat/usbd-event-calls
Nov 2, 2026
Merged

oneandonlydean merged 3 commits into
boykopovar:mainfrom
MaxLikesCode:feat/usbd-event-calls

Conversation

@MaxLikesCode

@MaxLikesCode MaxLikesCode commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

Implements the nine remaining sceUsbd placeholders, the event calls, with the locking model of FreeBSD 11.4 libusb10_io.c over the host libusb: sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEvents, sceUsbdLockEventWaiters, sceUsbdUnlockEventWaiters, sceUsbdWaitForEvent, sceUsbdEventHandlerActive, sceUsbdHandleEvents and sceUsbdHandleEventsLocked. Their signatures were already in the file, from orbital's console type database.

FreeBSD's model, which upstream libusb (the host's) does not share:

  • One lock serves both the events and their waiters.
  • The thread that takes it while no thread handles events becomes the handler. sceUsbdEventHandlingOk reports whether the caller is the handler, and sceUsbdEventHandlerActive whether any thread is.
  • sceUsbdTryLockEvents returns 1 when the lock is busy, held by the caller (FreeBSD's pthread_mutex_trylock gives EBUSY there) or when another thread handles events; otherwise it takes the lock and the handler role and returns 0.
  • sceUsbdUnlockEvents gives up the handler role, wakes all waiters and releases the lock.
  • sceUsbdWaitForEvent waits on that wake-up with the lock held, returns 1 on timeout and 0 otherwise, and takes the handler role if no thread has it.
  • sceUsbdHandleEventsLocked handles events if the caller is the handler, releasing the lock while the host libusb handles them and runs the transfer callbacks. Otherwise it waits and returns 0x80240007 (timeout) when the wait times out.
  • After each pass of event handling, a timed-out one included, the handler wakes all waiters, as libusb10_handle_events_sub does with pthread_cond_broadcast (L223-L224); a waiter in sceUsbdWaitForEvent or sceUsbdHandleEventsLocked then returns 0.
  • sceUsbdHandleEvents locks, handles events without a timeout and unlocks.

Changes to existing calls:

  • sceUsbdEventHandlingOk returned 1 for every thread. It now reports the handler role, as FreeBSD's libusb_event_handling_ok does, and the existing check in guest_usbd expects 0 from a thread without the lock.
  • sceUsbdHandleEventsTimeout now takes the same lock and handler path, so a second thread handling events waits for the first instead of entering the host libusb at the same time. Its argument checks are unchanged, and a timeout of more than INT_MAX seconds now throws.

These throw, as FreeBSD's result there is undefined or unknown:

  • taking the lock twice in one thread;
  • releasing it, waiting or calling sceUsbdHandleEventsLocked without holding it;
  • a negative or oversized timeout in sceUsbdWaitForEvent and sceUsbdHandleEventsLocked.

With no timeout, FreeBSD's poll returns on any event of its descriptors, including the wake-up a submit from another thread writes. Here event handling without a timeout returns once a transfer completes, in one-second host slices. Callers loop on these calls, but the difference is recorded.

docs/dev/debt/libSceUsbd.md drops "sceUsbdEventHandlingOk returns 1" and the empty unknown section, and adds one entry for the event calls. The library's 2214-character first entry is split into three entries of under 1000 characters with the same text, except that transfer callbacks now run "from event handling" rather than only from sceUsbdHandleEventsTimeout.

Tested

Windows 11 Pro 26200, WinLibs GCC 15.2.0 r7, full Release build with BUILD_TESTING=ON, this branch on #9190 and #9338, merged onto main b00235d.

  • guest_usbd gains EventLocks, which runs without a device:
    • lock, unlock and try-lock from the holder and from another thread, and the handler and handler-active answers on both;
    • a waiter that times out with no handler takes over handling;
    • a waiter queued while the handler is handling events is woken by the handler's unlock and becomes the handler;
    • a thread that holds the lock but is not the handler returns 0 from sceUsbdHandleEventsLocked when a pass of the handler ends, gets 0x80240007 when its timeout ends during a longer pass, and returns after the handler keeps the event lock past its timeout. The handler releases the lock until that thread has returned, and only then joins it;
    • each misuse above throws.
      It passes 3 of 3, and 56 of 56 with 8 copies at once (rechecked at bda456a). With Export.cpp from the device calls PR and the new tests, it fails at the first changed answer (a thread without the event lock may handle events). With the earlier ordering, where the handler joined that thread while holding the event lock, the held-lock case hangs (3 of 3 runs stopped at 20 s).
  • guest_usbd_tests device with APS5_USBD_DEVICE=1b1c:0c1a (CORSAIR Lighting Node CORE) completes an asynchronous control transfer through sceUsbdHandleEvents, and another under sceUsbdLockEvents through sceUsbdHandleEventsLocked, besides the existing transfer and cancel-race checks through sceUsbdHandleEventsTimeout. It passes 11 of 11 runs.
  • python tools/select_tests.py ctest build --output-on-failure --timeout 180 --parallel 8: 1523 of the 1526 selected tests pass. The 3 failures are agc_driver_sample_order, agc_driver_sample_order_grid_reference and agc_driver_sample_order_recipe_depth_locations, which fail the same way on main on this RTX 4090 and which test(agc): read the sample order tests' depth through a sampled copy #9056 fixes. agc_driver_image_array_window_past_layer_limit is not built: it does not compile with MinGW on main (test(agc): align the array window test's texels at run time #8310).
  • python tools/check_conventions.py --base origin/main passes. python tools/pr_overlap.py --branch feat/usbd-event-calls reports only feat(libSceUsbd): pass configuration and descriptor queries through to libusb #9190, which this builds on. test(libSceUsbd): cover sceUsbdFillControlSetup buffer formatting #9116 adds test lines to GuestUsbd.cpp too; the two merge cleanly.
  • @simnJS ran guest_usbd_tests device from feat(libSceUsbd): handle events with FreeBSD libusb's event lock and handler thread #9339's branch, which contains feat(libSceUsbd): pass the device, packet size, alternate setting and halt calls through to libusb #9338, on Windows 11 Pro 26200 (comment): an MSI Mystic Light HID controller (1462:7D75, full speed) and a Wooting 80HE keyboard (31E3:1402, high speed) pass 3 of 3 each, and sceUsbdDetachKernelDriver returns 0x802400FF on both. On the Wooting the iso packet size check takes the multiplier path above full speed.
  • @xXPBSXx built guest_usbd_tests on Linux (WSL2, Ubuntu 24.04, GCC 13.3, Release) from this head merged onto main df2569d (comment): it exits with 0, and with Export.cpp reset to main it aborts (SIGABRT).
  • Rebased onto feat(libSceUsbd): pass the device, packet size, alternate setting and halt calls through to libusb #9338's rebased head 64ae578 after refactor(libs): add full export headers for libs #10706; the review fix is now 6c20825 (was bda456a). guest_usbd passes 3 of 3, and 56 of 56 with 8 copies at once, and fails 3 of 3 with Export.cpp from the device calls PR. The device run with APS5_USBD_DEVICE=1b1c:0c1a passes 2 of 2. public_headers and the conventions check pass.
  • @simnJS ran head 6c20825 merged onto main 750cf6c (Windows 11 Pro 26200, Ryzen 9 7950X3D, WinLibs GCC 15.2.0 r7, Release; comment): guest_usbd 20 of 20; guest_usbd_tests device 5 of 5 on 1462:7D75 and 5 of 5 on 31E3:1402; a Logitech LIGHTSPEED receiver 046D:C54D passes the new sceUsbdHandleEvents and sceUsbdHandleEventsLocked transfers and stops at the existing cancel-race check, as it does with main's Export.cpp and GuestUsbd.cpp; guest_usb_storage, guest_system_service_usb_storage and guest_usb_storage_dialog 5 of 5 each. They also found that waiters woke only at the handler's unlock, where FreeBSD wakes them after each pass.
  • Review fix 9f3799c, rebased onto main d5ef780 (the two earlier commits are now dbb27b9 and c0d8f0a): the handler wakes waiters after each pass, and guest_usbd's follower case is split into the three cases above. With the new test and Export.cpp without the change, guest_usbd fails 3 of 3 at a thread that is not the handler was not woken when a pass of event handling ended (the follower returns only at its 5 s timeout). With the change it passes 10 of 10, and 56 of 56 with 8 copies at once. guest_usbd_tests device with APS5_USBD_DEVICE=1b1c:0c1a passes 3 of 3. The full suite passes 2827 of 2828; only guest_wide_formatting fails, the same way as on main on this machine (fix(libc): convert the Windows time zone name from the ANSI code page to UTF-8 for tzname, %Z and tm_zone #10981, German code page, fixed by fix(libc): convert Windows time zone names to UTF-8 #10987). python tools/check_conventions.py --base origin/main passes.
  • Not tested: a console, a title, bare-metal Linux or macOS.

Checklist

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Progress report (04fefeb → 9f3799c)

📈 System libraries: 46.73% (+0.07%, +9 implemented functions)

✅ 9 implemented
Library Name
libSceUsbd sceUsbdEventHandlerActive
libSceUsbd sceUsbdHandleEvents
libSceUsbd sceUsbdHandleEventsLocked
libSceUsbd sceUsbdLockEventWaiters
libSceUsbd sceUsbdLockEvents
libSceUsbd sceUsbdTryLockEvents
libSceUsbd sceUsbdUnlockEventWaiters
libSceUsbd sceUsbdUnlockEvents
libSceUsbd sceUsbdWaitForEvent

Changed libraries (implemented exports)

Library Before After Change
libSceUsbd 52/61 (85.25%) 61/61 (100.00%) +14.75 pp

@simnJS

simnJS commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Device run of #9339's head be52eb439 (on #9338 b77a11079 and #9190 66f467e40). Windows 11 Pro 26200, Ryzen 9 7950X3D, WinLibs GCC 15.2.0 r7, Release. ctest -R guest_usbd passes 3 of 3.

Device APS5_USBD_DEVICE Windows driver guest_usbd_tests device Speed Alt setting 0 sceUsbdDetachKernelDriver
MSI Mystic Light HID controller 1462:7D75 HidUsb 3 of 3 pass, exit 0 (0.45–0.52 s) 2 (full) 0 0x802400FF
Wooting 80HE keyboard 31E3:1402 usbccgp (HID interfaces) 3 of 3 pass, exit 0 (0.45–0.47 s) 3 (high) 0 0x802400FF

Both runs print USBD device tests passed and the product strings MYSTIC LIGHT and Wooting 80HE. sceUsbdDetachKernelDriver returns the other error on both devices, which the test accepts.

AI-assisted: yes

@MaxLikesCode

Copy link
Copy Markdown
Contributor Author

Thanks @simnJS. The Wooting is the first high-speed device on these tests, so its run is also the first where sceUsbdGetMaxIsoPacketSize takes the multiplier path above full speed. I added both devices to the Tested sections here and on #9338.

AI-assisted: yes (Claude Code)

@xXPBSXx

xXPBSXx commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Linux data point for be52eb4396 (WSL2, Ubuntu 24.04, kernel 6.18 - a virtualised kernel, not bare-metal Linux; GCC 13.3, Release, BUILD_TESTING=ON), this head merged onto main df2569d2c5 (clean merge). I built guest_usbd_tests and ran the executable directly (exit code, not through ctest):

AI-assisted (Claude Code).

@MaxLikesCode

Copy link
Copy Markdown
Contributor Author

Thanks @xXPBSXx. These are the first Linux runs of the USBD tests, and the first to check on Linux that they fail without the change. I added them to the Tested sections here and on #9338.

AI-assisted: yes (Claude Code)

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@MaxLikesCode GuestUsbd.cpp:176 takes the event mutex, HandleEventsLocked at :188 reacquires it, and follower.join at :189 runs before unlock at :191. If the follower’s condition-variable timeout expires but its mutex reacquisition is delayed, it cannot finish while the main thread waits in join. Could you release the event mutex while establishing follower completion, retain the timeout assertion, and add a bounded regression that forces delayed reacquisition? The 100 ms/30 ms timing difference does not establish that ordering.

@MaxLikesCode

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks, that ordering could deadlock. The main thread joined the follower while holding the event mutex, so a follower whose 30 ms wait had timed out, but which had not yet reacquired the mutex, could not return. bda456a changes the test:

  • The case moves into FollowerTimesOut. After the follower has queued, the handler keeps calling sceUsbdHandleEventsLocked in 20 ms slices, which release the event mutex, until the follower reports that it has returned and released the lock (bounded at 10 s). Only then does the handler join it. The 0x80240007 timeout assertion stays.
  • The case runs a second time with the handler holding the event mutex for 100 ms after the follower has queued, past the follower's 30 ms timeout, so the follower's reacquisition is always delayed.
  • With the earlier ordering (join while holding the mutex), that second case hangs: 3 of 3 runs stopped at 20 s. With the change, guest_usbd passes 3 of 3, and 56 of 56 with 8 copies at once.

AI-assisted: yes (Claude Code)

@simnJS

simnJS commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

Windows run and one review point for head 6c208252d7, merged onto main 750cf6cf48 (clean merge). Windows 11 Pro 26200, Ryzen 9 7950X3D, WinLibs GCC 15.2.0 r7, Release, BUILD_TESTING=ON.

Runs on this merge

  • guest_usbd without a device: 20 of 20 pass (ctest -R "^guest_usbd$" --repeat until-fail:10, then the executable 10 times, 0.43 to 0.61 s).
  • guest_usbd_tests device, 5 runs per device:
    • MSI Mystic Light 1462:7D75 (full speed): 5 of 5 pass, 0.53 to 0.63 s.
    • Wooting 80HE 31E3:1402 (high speed): 5 of 5 pass, 0.50 to 0.60 s.
    • Logitech LIGHTSPEED receiver 046D:C54D: passes the new sceUsbdHandleEvents and sceUsbdHandleEventsLocked transfer checks, then stops at the existing cancel-race check (0xc0000409, 5 of 5). With main's Export.cpp and GuestUsbd.cpp it stops at the same check (3 of 3), so this is not from this PR.
  • guest_usb_storage, guest_system_service_usb_storage and guest_usb_storage_dialog: 5 of 5 each.

Review point: waiters only wake when the handler unlocks

FreeBSD's libusb10_handle_events_sub ends every pass with pthread_cond_broadcast(&ctx->ctx_cond) ("Wakeup other waiters", libusb10_io.c L223-L224), after the done callbacks. A pass that times out gets there too, through do_done (L148-L156). So a thread in libusb_wait_for_event wakes after each pass of the handler. HandleAsHandler takes events.lock back without notifying changed, so here a waiter only wakes when the handler calls sceUsbdUnlockEvents.

Measured on this merge with a local program (not part of the PR):

  • One thread takes sceUsbdLockEvents and calls sceUsbdHandleEventsLocked with a 20 ms timeout for 600 ms. Another calls sceUsbdLockEventWaiters, then sceUsbdWaitForEvent(NULL). It returns at 610 to 621 ms (3 runs), when the handler unlocks, and becomes the handler.
  • The same with a 2 s handler loop and a device. The waiter submits a GET_DESCRIPTOR control transfer and loops on sceUsbdWaitForEvent(NULL) until its callback has run, as libusb's event-waiter pattern does. On 1462:7D75 and 31E3:1402 the callback runs within 1 ms of the submit, and the waiter returns at 2004 to 2019 ms (3 runs per device).
  • With events.changed.notify_all(); added after events.owner.store(self); at the end of HandleAsHandler, the first case returns at 20 to 25 ms (3 runs) without taking the handler role. In the device case (1 run, 1462:7D75) the waiter returns in the same millisecond as the callback.

With that line, guest_usbd fails 2 of 23 runs here at "a thread that is not the handler did not time out in sceUsbdHandleEventsLocked". In FollowerTimesOut(false), the follower's 30 ms timeout then races the end of the handler's 20 ms pass, which wakes it with 0, as FreeBSD's libusb_wait_for_event would. The debt entry's "Waiters wake when the handler unlocks" would need "and after each pass of event handling".

Not run: Linux, macOS, a console or title, the rest of the suite.

AI-assisted: yes

MaxLikesCode and others added 3 commits October 28, 2026 15:19
…handler thread

sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEvents,
sceUsbdLockEventWaiters, sceUsbdUnlockEventWaiters, sceUsbdWaitForEvent,
sceUsbdEventHandlerActive, sceUsbdHandleEvents and
sceUsbdHandleEventsLocked follow FreeBSD 11.4 libusb10_io.c: one lock
for events and their waiters, one handler thread that releases the lock
while the host libusb handles events, and waiters that are woken when
the handler unlocks and take over handling when there is none.
sceUsbdEventHandlingOk now reports whether the calling thread is the
handler, and sceUsbdHandleEventsTimeout takes the same path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ishes

The timed-out follower case joined the follower while the main thread
held the event lock, so a follower whose timeout had expired but which
had not yet reacquired the lock could not return. Keep handling events
in slices, which release the lock, until the follower has returned, and
run the case a second time with the lock held past the follower's
timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FreeBSD 11.4's libusb10_handle_events_sub ends every pass, a timed-out
one included, with pthread_cond_broadcast on the context's condition
variable, so a thread in libusb_wait_for_event wakes after each pass of
the handler. Here waiters woke only when the handler unlocked, so a
waiter could sleep through the handler's whole loop. Notify them when
the handler takes the event lock back after a pass.

The follower test that expected a timeout while the handler ran 20 ms
passes now races that wake-up, so guest_usbd checks three cases
instead: a follower with a long timeout returns 0 after a pass, a
follower times out during a longer pass, and a follower returns after
the handler held the event lock past its timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxLikesCode
MaxLikesCode force-pushed the feat/usbd-event-calls branch from 6c20825 to 9f3799c Compare October 28, 2026 14:35
@MaxLikesCode

Copy link
Copy Markdown
Contributor Author

Thanks @simnJS for the runs and the review point; you were right. FreeBSD's libusb10_handle_events_sub ends every pass, a timed-out one included, with pthread_cond_broadcast(&ctx->ctx_cond) (L148-L156, L223-L224). 9f3799c notifies the waiters when the handler takes the event lock back after a pass, which is the line you tried, and the debt entry now says waiters wake after each handling pass and when the handler unlocks.

As you found, FollowerTimesOut(false) then raced that wake-up. guest_usbd now checks three cases instead:

  • a follower with a 5 s timeout returns 0 when a pass of the handler ends. Without the change it fails 3 of 3, because the follower returns only at its timeout;
  • a follower whose 30 ms timeout ends inside a 2 s pass of the handler gets 0x80240007;
  • a follower returns after the handler keeps the event lock past its timeout (the earlier hang). Only its return is checked, since whether it reports the timeout there depends on when it gets the lock back.

With the change, guest_usbd passes 10 of 10 and 56 of 56 with 8 copies at once, and guest_usbd_tests device on 1b1c:0c1a (CORSAIR Lighting Node CORE) passes 3 of 3. The full suite passes 2827 of 2828, with only guest_wide_formatting failing as it does on main here (#10981), and the conventions check passes. I also rebased onto main d5ef780 and added your runs to the Tested section.

AI-assisted: yes (Claude Code)

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 14212 docs/dev/debt/libSceUsbd.md

@oneandonlydean
oneandonlydean merged commit a98262c into boykopovar:main Nov 2, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants