Skip to content

Add native SCHED_FIFO promotion for the Linux build without dbus - #45

Merged
padenot merged 7 commits into
mozilla:masterfrom
HEnquist:native-rt-without-dbus
Jul 30, 2026
Merged

Add native SCHED_FIFO promotion for the Linux build without dbus#45
padenot merged 7 commits into
mozilla:masterfrom
HEnquist:native-rt-without-dbus

Conversation

@HEnquist

@HEnquist HEnquist commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Give the Linux no-dbus build a real implementation instead of a no-op:
promote the current thread directly with pthread_setschedparam(SCHED_FIFO).
The rtkit path and other platforms are untouched. See #44 for the rationale.

  • New rt_linux_native.rs, wired in via a dedicated cfg branch after the rtkit branch
  • Priority defaults to 10 (matching rtkit), overridable via the optional set_rt_priority() (1-99)
  • Restores the saved policy on demotion (keeps SCHED_RESET_ON_FORK set, which an
    unprivileged thread cannot clear)
  • Tests cover the RLIMIT_RTPRIO boundary and the priority override; the fallback CI
    job raises RLIMIT_RTPRIO so promotion runs for real
  • Docs: add a Platforms section and correct API docs that assumed D-Bus was the only
    Linux backend

Edit: the priority override was originally an AUDIO_RT_PRIORITY environment variable; it is now
the optional set_rt_priority() method instead (a library should not read env vars on its own).

HEnquist added 4 commits July 8, 2026 22:42
The Linux build without the `dbus` feature previously fell through to the
blanket fallback, where promotion was a no-op that returned Ok, so callers
believed they had real-time priority when they did not.

Give that build a real implementation: promote the current thread directly
with pthread_setschedparam(SCHED_FIFO | SCHED_RESET_ON_FORK) at priority 10,
restoring the saved policy on demotion. This needs no D-Bus daemon and works
whenever the process may request real-time scheduling (root, CAP_SYS_NICE, or
an RLIMIT_RTPRIO budget). The rtkit path and other platforms are untouched.

- New rt_linux_native.rs with the native backend, wired into a dedicated cfg
  branch after the rtkit branch.
- The requested priority defaults to 10 and can be overridden with the
  AUDIO_RT_PRIORITY environment variable (1-99).
- Tests exercise the RLIMIT_RTPRIO boundary and the priority override; the
  fallback CI job raises RLIMIT_RTPRIO so promotion runs for real.
- Docs: add a Platforms section and correct API docs that assumed D-Bus was
  the only Linux backend.
A library reading an environment variable it was never told about is a hidden
global input that can surprise the embedding application. Replace it with an
explicit, optional setter that the caller drives: set_rt_priority(Some(n))
overrides the default of 10, None restores it. Linux no-dbus only; if never
called, promotion uses priority 10 as before.
- Drop the `param` field from RtPriorityThreadInfoInternal so the struct has no
  padding, matching the rtkit path's struct. `serialize`'s transmute no longer
  reads uninitialized padding bytes (which was UB). Demotion now restores the
  saved policy with a zeroed param, as the rtkit path already does.
- rt_scheduling_available: treat a demotion failure after a successful promotion
  as fatal rather than ignoring it.
- Fix a stale test comment that referred to the removed environment variable.

@padenot padenot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good, one small question before we can merge. I'll tag a release when it's done.

Comment thread src/rt_linux_native.rs
It claimed the thread info was only meaningful within the same process, which
contradicts promote_thread_to_real_time_internal promoting a thread in another
process. The thread is identified by its system-wide tid, so cross-process
promotion works; correct the comment to say so.

@roderickvd roderickvd left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for putting in this PR. I stumbled on it as I was investigating the same thing on cpal. Hence my drive-by review 😄

Would this be SemVer-breaking for audio_thread_priority?

Comment thread src/rt_linux_native.rs Outdated

let rc = unsafe {
libc::sched_setscheduler(
thread_info.thread_id as libc::pid_t,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

thread_id is c_long and i64 on 64-bit Linux. Casting it to i32 pid_t is probably safe, but defensively it could make sense to keep it in its original width?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

sched_setscheduler takes pid_t (i32) so we narrow at the syscall regardless, but you're right it's cleaner to be explicit. Switched to a checked try_from in f34b7be. The field stays c_long to mirror the rtkit struct.

Comment thread src/rt_linux_native.rs Outdated
let rc = unsafe {
libc::sched_setscheduler(
thread_info.thread_id as libc::pid_t,
thread_info.policy | SCHED_RESET_ON_FORK,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This restores the original policy, whereas the D-Bus version hardcodes SCHED_OTHER. I like yours better... regardless of which it'll be, consistency would be a good thing to have.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, and the dbus path is inconsistent with itself too: current-thread demote restores the saved policy, remote demote hardcodes SCHED_OTHER. I'd rather just make everything restore the saved policy here in this PR than split it into a follow-up. It touches the dbus path though, so @padenot your call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in ac3a784. The rtkit remote demote now restores the saved policy too, so both paths (and both the current-thread and remote demotes) are consistent.

Comment thread src/rt_linux_native.rs Outdated
// Keep SCHED_RESET_ON_FORK set: the kernel forbids an unprivileged thread from clearing that
// flag once set (and promotion set it), so restoring the bare saved policy would fail with
// EPERM. The flag is harmless on a non-real-time thread.
let param = unsafe { std::mem::zeroed::<libc::sched_param>() };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This breaks if a thread already was SCHED_FIFO or SCHED_RR with nonzero priority before we promoted it. I think it's the same in the D-Bus version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, same as the dbus version. I saved and restored the full param in an earlier revision but dropped it to fix a padding-UB in the serialized struct. I can bring it back and restore the priority faithfully (with padding-safe serialization). Since it's the same issue in both paths, I'd fix it in both rather than non-dbus only. @padenot, ok to do that here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes please.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in ac3a784. Both paths now capture the priority in get_current_thread_info and restore it on demote. Adding the field gives the struct tail padding, so I switched serialize/deserialize to explicit byte packing instead of transmuting, so no uninitialized padding is read. ATP_THREAD_INFO_SIZE goes 24 to 32 as a result.

sched_setscheduler takes pid_t (i32), so the tid (c_long) must be narrowed at
the syscall. A tid always fits, but convert with try_from and surface an error
instead of truncating silently.
@HEnquist

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look! Not SemVer-breaking API-wise, it only adds set_rt_priority. The one behavior change is on the no-dbus build, which used to no-op and return Ok and now actually promotes (and can return Err). A minor bump would make sense to signal that, but the version is @padenot's call, and may depend on how the other comments land.

@roderickvd

Copy link
Copy Markdown

Thanks for the quick adjustments too! I would also prefer fixing the consistency in one swoop, over the administrative burden of split PRs.

I wouldn’t mind a SemVer bump because the behavior would be different on non-dbus targets. And I could then adjust the documentation accordingly in cpal, when Cargo and I can clearly discern between two releases.

@padenot

padenot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Yes, let's get everything fixed here and now, thanks a lot for the comments and prompt patch updates.

I'll do a SemVer bump (minor) as well right after merging this.

Demotion restored the saved policy but always with priority 0, so a thread that
was already SCHED_FIFO/RR with a nonzero priority before promotion could not be
restored (it would fail with EINVAL). Capture the priority in
get_current_thread_info and restore it on demotion, in both the native and rtkit
paths.

Also make the rtkit remote demote restore the saved policy instead of hardcoding
SCHED_OTHER, matching the native path and the current-thread demote.

Storing the extra field gives the info struct tail padding, so serialize the
fields explicitly (chaining their bytes) instead of transmuting the whole struct,
which would read the uninitialized padding bytes.

@padenot padenot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks a lot for the extra roundtrip here. Merging and doing a release now.

Comment thread src/rt_linux_native.rs Outdated
// Keep SCHED_RESET_ON_FORK set: the kernel forbids an unprivileged thread from clearing that
// flag once set (and promotion set it), so restoring the bare saved policy would fail with
// EPERM. The flag is harmless on a non-real-time thread.
let param = unsafe { std::mem::zeroed::<libc::sched_param>() };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes please.

@padenot
padenot merged commit 1661371 into mozilla:master Jul 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants