mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
fix(nesinit): give the audio graph realtime priority by rlimit, not rtkit
Inside a box, every PipeWire, wireplumber and neswire thread was running SCHED_OTHER. The rtkit -> polkit -> system bus chain started but granted nothing. With a game keeping the vCPUs busy, the graph's 5 ms cycles ran late. pw-top counted no xruns, but neswire sent packets in bursts, and the client saw 15-25 ms gaps in audio alone (video arriving on time in the same seconds), which emptied its 10 ms cushion every 30 s or so. pipewire and neswire now exec with RLIMIT_RTPRIO 95, set before the uid drop. module-rt checks that first and sets SCHED_FIFO itself, which is how PipeWire is meant to run outside a desktop session. The workload doesn't get the grant, so a game can't take the CPU away from the graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
87092d4693
commit
36f13bfcfc
@@ -119,6 +119,21 @@ pub struct Service {
|
||||
/// socket list lives inside a module's arguments, and a drop-in that
|
||||
/// re-declares that module loads it twice.
|
||||
pub umask: Option<u32>,
|
||||
/// Whether this service may give its own threads a realtime class, up to
|
||||
/// [`REALTIME_PRIORITY`].
|
||||
///
|
||||
/// For the two processes that carry the audio graph's 5 ms cycle. Granted
|
||||
/// as `RLIMIT_RTPRIO` at exec, which PipeWire's `module-rt` checks first
|
||||
/// and uses to set `SCHED_FIFO` itself. The grant used to come from rtkit,
|
||||
/// asked over the system bus with polkit's approval, and in a box that
|
||||
/// chain granted nothing. Measured 2026-10-16: every PipeWire, wireplumber
|
||||
/// and neswire thread was `SCHED_OTHER`, and cycles that ran late behind a
|
||||
/// busy game reached the client as 15-25 ms gaps in the stream, which
|
||||
/// emptied its 10 ms cushion.
|
||||
///
|
||||
/// Not given to the workload: a game that gave itself a realtime class
|
||||
/// could starve the graph that this exists to protect.
|
||||
pub realtime: bool,
|
||||
/// A path that exists once this service can be talked to.
|
||||
///
|
||||
/// `None` means "started is ready", which is true of anything nothing else
|
||||
@@ -181,6 +196,14 @@ pub const AUDIO_DIR: &str = "/run/pipewire";
|
||||
/// Where a service's runtime sockets live.
|
||||
pub const RUNTIME_DIR: &str = "/run/user/1000";
|
||||
|
||||
/// The highest realtime priority a [`Service::realtime`] service may take.
|
||||
///
|
||||
/// PipeWire's own packaging grants its group 95 (`limits.d/25-pw-rlimits.conf`),
|
||||
/// and `module-rt` asks for 88 on the graph and 83 on clients. A limit below
|
||||
/// what it asks makes it clamp rather than fail, so 95 just means it gets
|
||||
/// what it asks for.
|
||||
pub const REALTIME_PRIORITY: libc::rlim_t = 95;
|
||||
|
||||
/// Somewhere every service may write.
|
||||
///
|
||||
/// # The service user's home is on a read-only root
|
||||
@@ -235,6 +258,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "nothing that speaks on the system bus can find it",
|
||||
required: true,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
// Now something does connect to it: the two services below, once each
|
||||
// as they start, and neither one retries. A bus that has been forked and
|
||||
// has not bound its socket yet is a connection refused to both.
|
||||
@@ -262,6 +286,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "rtkit cannot be authorised, so audio runs at normal priority",
|
||||
required: false,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
ready: None,
|
||||
ready_name: Some("org.freedesktop.PolicyKit1"),
|
||||
},
|
||||
@@ -273,6 +298,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "audio threads cannot get realtime priority and underrun when the CPUs are busy",
|
||||
required: false,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
ready: None,
|
||||
ready_name: Some("org.freedesktop.RealtimeKit1"),
|
||||
},
|
||||
@@ -290,6 +316,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "audio and anything else expecting a session bus will not start",
|
||||
required: true,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
// Every service after this one is handed this path as its bus address,
|
||||
// and a bus address that is not bound yet is a service that starts,
|
||||
// finds nothing, and carries on without a bus.
|
||||
@@ -309,6 +336,7 @@ pub const STACK: &[Service] = &[
|
||||
required: true,
|
||||
// So the workload, which is not this user, can open the socket.
|
||||
umask: Some(0),
|
||||
realtime: true,
|
||||
// Both the session manager and the sender connect here, and so does
|
||||
// the workload once it starts.
|
||||
ready: Some("/run/pipewire/pipewire-0"),
|
||||
@@ -329,6 +357,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "audio devices exist but nothing routes them",
|
||||
required: false,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
@@ -344,6 +373,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "the client gets pictures and no sound",
|
||||
required: false,
|
||||
umask: None,
|
||||
realtime: true,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
@@ -357,6 +387,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "the session has no address, so no client can reach it",
|
||||
required: true,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
@@ -373,6 +404,7 @@ pub const STACK: &[Service] = &[
|
||||
cost: "controllers plugged into the client do not reach the game",
|
||||
required: false,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
// It dials the hub rather than the other way round, and redials until
|
||||
// the hub is there, so nothing waits on it.
|
||||
ready: None,
|
||||
@@ -505,10 +537,11 @@ impl Stack {
|
||||
command.envs(service.env.iter().copied());
|
||||
|
||||
let mask = service.umask;
|
||||
let realtime = service.realtime;
|
||||
if let Some((uid, gid)) = service.user {
|
||||
// SAFETY: the closure runs between fork and exec in the child,
|
||||
// where only async-signal-safe calls are allowed. These two are,
|
||||
// and it allocates nothing.
|
||||
// where only async-signal-safe calls are allowed. These are, and it
|
||||
// allocates nothing.
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
if let Some(mask) = mask {
|
||||
@@ -516,6 +549,17 @@ impl Stack {
|
||||
// child, between fork and exec.
|
||||
libc::umask(mask as libc::mode_t);
|
||||
}
|
||||
// Before the uid drops: raising a hard limit takes root,
|
||||
// and once dropped the child could never get it back.
|
||||
if realtime {
|
||||
let limit = libc::rlimit {
|
||||
rlim_cur: REALTIME_PRIORITY,
|
||||
rlim_max: REALTIME_PRIORITY,
|
||||
};
|
||||
if libc::setrlimit(libc::RLIMIT_RTPRIO, &limit) != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
// gid first: dropping the uid first would lose the
|
||||
// privilege needed to set the gid at all.
|
||||
if libc::setgid(gid) != 0 {
|
||||
@@ -1164,6 +1208,25 @@ mod tests {
|
||||
assert!(at("rtkit") < at("pipewire"));
|
||||
}
|
||||
|
||||
/// The graph and the sender run its 5 ms cycle and may go realtime; nothing
|
||||
/// else in the table may, because a realtime thread that is not the graph
|
||||
/// is one that can take the CPU away from it.
|
||||
#[test]
|
||||
fn only_the_audio_graph_may_go_realtime() {
|
||||
let realtime: Vec<_> = STACK
|
||||
.iter()
|
||||
.filter(|s| s.realtime)
|
||||
.map(|s| s.name)
|
||||
.collect();
|
||||
assert_eq!(realtime, ["pipewire", "neswire"]);
|
||||
assert!(
|
||||
STACK
|
||||
.iter()
|
||||
.filter(|s| s.realtime)
|
||||
.all(|s| s.user.is_some())
|
||||
);
|
||||
}
|
||||
|
||||
/// A required service whose absence has a workaround should not be
|
||||
/// required, and an optional one whose absence has none should not be
|
||||
/// optional. Only the second half is checkable, and it is the one that
|
||||
|
||||
@@ -20,6 +20,7 @@ static SLEEPERS: &[Service] = &[
|
||||
cost: "nothing: this is a test",
|
||||
required: true,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
@@ -31,6 +32,7 @@ static SLEEPERS: &[Service] = &[
|
||||
cost: "nothing: this is a test",
|
||||
required: true,
|
||||
umask: None,
|
||||
realtime: false,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user