mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
feat: RT for audio
This commit is contained in:
+163
-10
@@ -130,6 +130,15 @@ pub struct Service {
|
||||
/// Existence only. Whether the thing behind the socket answers correctly is
|
||||
/// not knowable from here, and a box is not the place to find out.
|
||||
pub ready: Option<&'static str>,
|
||||
/// A name on the system bus that exists once this service can be talked to,
|
||||
/// for the ones that are reached by name and bind no socket of their own.
|
||||
///
|
||||
/// Same reason as `ready` and the same rule: `spawn` returns when the child
|
||||
/// has been forked, which is before it has connected to the bus and asked
|
||||
/// for its name, and whatever starts next would ask a bus that has no one
|
||||
/// by that name. For realtime audio the cost is permanent: PipeWire asks
|
||||
/// once, as it starts, and a refusal is not retried.
|
||||
pub ready_name: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// How long a service gets to bind its socket before the box gives up on it.
|
||||
@@ -226,7 +235,46 @@ pub const STACK: &[Service] = &[
|
||||
cost: "nothing that speaks on the system bus can find it",
|
||||
required: true,
|
||||
umask: None,
|
||||
// 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.
|
||||
ready: Some("/run/dbus/system_bus_socket"),
|
||||
ready_name: None,
|
||||
},
|
||||
// Realtime priority for audio, which is two daemons because the one that
|
||||
// grants it asks the other whether to.
|
||||
//
|
||||
// PipeWire runs as an ordinary user and a realtime scheduling class is not
|
||||
// something an ordinary user may give itself, so its threads run at normal
|
||||
// priority and miss their deadlines whenever a game keeps every CPU busy:
|
||||
// the symptom is crackling, with a counter of xruns climbing in the audio
|
||||
// graph. The grant comes from rtkit over the system bus, and rtkit will not
|
||||
// grant anything without polkit saying yes, so both are here, polkit first.
|
||||
//
|
||||
// Both start as root and drop their own privileges, so neither is a third
|
||||
// user in this table. Both are optional: without them the box has audio at
|
||||
// normal priority, which is the state it was in before they existed.
|
||||
Service {
|
||||
name: "polkitd",
|
||||
argv: &["/usr/lib/polkit-1/polkitd", "--no-debug"],
|
||||
env: &[],
|
||||
user: None,
|
||||
cost: "rtkit cannot be authorised, so audio runs at normal priority",
|
||||
required: false,
|
||||
umask: None,
|
||||
ready: None,
|
||||
ready_name: Some("org.freedesktop.PolicyKit1"),
|
||||
},
|
||||
Service {
|
||||
name: "rtkit",
|
||||
argv: &["/usr/lib/rtkit-daemon"],
|
||||
env: &[],
|
||||
user: None,
|
||||
cost: "audio threads cannot get realtime priority and underrun when the CPUs are busy",
|
||||
required: false,
|
||||
umask: None,
|
||||
ready: None,
|
||||
ready_name: Some("org.freedesktop.RealtimeKit1"),
|
||||
},
|
||||
Service {
|
||||
name: "dbus-session",
|
||||
@@ -246,6 +294,7 @@ pub const STACK: &[Service] = &[
|
||||
// and a bus address that is not bound yet is a service that starts,
|
||||
// finds nothing, and carries on without a bus.
|
||||
ready: Some("/run/user/1000/bus"),
|
||||
ready_name: None,
|
||||
},
|
||||
Service {
|
||||
name: "pipewire",
|
||||
@@ -263,6 +312,7 @@ pub const STACK: &[Service] = &[
|
||||
// Both the session manager and the sender connect here, and so does
|
||||
// the workload once it starts.
|
||||
ready: Some("/run/pipewire/pipewire-0"),
|
||||
ready_name: None,
|
||||
},
|
||||
Service {
|
||||
name: "wireplumber",
|
||||
@@ -280,6 +330,7 @@ pub const STACK: &[Service] = &[
|
||||
required: false,
|
||||
umask: None,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
Service {
|
||||
name: "neswire",
|
||||
@@ -294,6 +345,7 @@ pub const STACK: &[Service] = &[
|
||||
required: false,
|
||||
umask: None,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
Service {
|
||||
name: "neshub",
|
||||
@@ -306,6 +358,7 @@ pub const STACK: &[Service] = &[
|
||||
required: true,
|
||||
umask: None,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
Service {
|
||||
name: "nesgamepad",
|
||||
@@ -323,6 +376,7 @@ pub const STACK: &[Service] = &[
|
||||
// It dials the hub rather than the other way round, and redials until
|
||||
// the hub is there, so nothing waits on it.
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -382,19 +436,27 @@ impl Stack {
|
||||
/// rule above decides what it costs: a required service that never binds refuses
|
||||
/// the box, an optional one is stepped over.
|
||||
fn await_ready(&self, service: &Service, before: Option<Identity>) -> Result<(), Failure> {
|
||||
let Some(path) = service.ready else {
|
||||
return Ok(());
|
||||
};
|
||||
// The watch for what was just started, so a service that dies during its
|
||||
// own bring-up is not waited out for the full timeout.
|
||||
let started = self.running.last().map(|(_, watched)| watched.pid);
|
||||
await_path(
|
||||
service.name,
|
||||
path,
|
||||
before,
|
||||
&|| started.is_none_or(is_alive),
|
||||
READY_TIMEOUT,
|
||||
)
|
||||
if let Some(path) = service.ready {
|
||||
await_path(
|
||||
service.name,
|
||||
path,
|
||||
before,
|
||||
&|| started.is_none_or(is_alive),
|
||||
READY_TIMEOUT,
|
||||
)?;
|
||||
}
|
||||
if let Some(name) = service.ready_name {
|
||||
await_bus_name(
|
||||
service.name,
|
||||
name,
|
||||
&|| started.is_none_or(is_alive),
|
||||
READY_TIMEOUT,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn(&mut self, service: &'static Service) -> Result<(), Failure> {
|
||||
@@ -492,6 +554,59 @@ impl Stack {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `reply` is the system bus saying a name has an owner.
|
||||
///
|
||||
/// The answer to `NameHasOwner` as `dbus-send` prints it, a line that reads
|
||||
/// `boolean true` or `boolean false`. Anything else, including nothing at all, is
|
||||
/// not an owner.
|
||||
fn bus_says_owned(reply: &str) -> bool {
|
||||
reply.lines().any(|line| line.trim() == "boolean true")
|
||||
}
|
||||
|
||||
/// Wait for `name` to have an owner on the system bus.
|
||||
///
|
||||
/// Asked of the bus itself with `dbus-send`, which the image has because the bus
|
||||
/// does: a program that connects, asks and goes is the least that can be done
|
||||
/// here without init growing a bus client of its own. Polled for the same reason
|
||||
/// [`await_path`] is, and ends early the same way, when what was started has died.
|
||||
fn await_bus_name(
|
||||
service: &str,
|
||||
name: &str,
|
||||
alive: &dyn Fn() -> bool,
|
||||
timeout: std::time::Duration,
|
||||
) -> Result<(), Failure> {
|
||||
let deadline = std::time::Instant::now() + timeout;
|
||||
loop {
|
||||
let reply = std::process::Command::new("/usr/bin/dbus-send")
|
||||
.args([
|
||||
"--system",
|
||||
"--print-reply",
|
||||
"--dest=org.freedesktop.DBus",
|
||||
"/org/freedesktop/DBus",
|
||||
"org.freedesktop.DBus.NameHasOwner",
|
||||
&format!("string:{name}"),
|
||||
])
|
||||
.output();
|
||||
if let Ok(out) = reply
|
||||
&& bus_says_owned(&String::from_utf8_lossy(&out.stdout))
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if !alive() {
|
||||
return Err(Failure::new(format!(
|
||||
"{service} exited before it took the name {name} on the system bus"
|
||||
)));
|
||||
}
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return Err(Failure::new(format!(
|
||||
"{service} did not take the name {name} on the system bus within {}s",
|
||||
timeout.as_secs()
|
||||
)));
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(25));
|
||||
}
|
||||
}
|
||||
|
||||
impl Services for Stack {
|
||||
fn bring_up(&mut self, video: VideoLimits) -> Result<Vec<String>, Failure> {
|
||||
self.video = video;
|
||||
@@ -782,6 +897,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_an_owned_name_counts_as_ready() {
|
||||
assert!(bus_says_owned(
|
||||
"method return time=1.0 sender=org.freedesktop.DBus -> destination=:1.4\n boolean true\n"
|
||||
));
|
||||
assert!(!bus_says_owned(" boolean false\n"));
|
||||
assert!(!bus_says_owned(""), "a failed dbus-send says nothing");
|
||||
}
|
||||
|
||||
/// Both of the realtime-audio daemons talk to the system bus once, as they
|
||||
/// start, and neither retries, so that bus has to have said it is there.
|
||||
#[test]
|
||||
fn the_system_bus_is_waited_for_before_what_connects_to_it() {
|
||||
let bus = STACK.iter().find(|s| s.name == "dbus-system").unwrap();
|
||||
assert!(bus.ready.is_some(), "polkitd and rtkit would find no socket");
|
||||
for name in ["polkitd", "rtkit"] {
|
||||
let svc = STACK.iter().find(|s| s.name == name).unwrap();
|
||||
assert!(svc.ready_name.is_some(), "{name} is reached by name");
|
||||
}
|
||||
}
|
||||
|
||||
/// A service that names no socket is ready when it has been started, and
|
||||
/// the wait has to be free in that case: most of the table is like this.
|
||||
#[test]
|
||||
@@ -1002,6 +1138,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The grant has to be in place before the audio server asks for it: PipeWire
|
||||
/// asks once, when it starts, and a daemon that comes up after it is a grant
|
||||
/// that was never asked for. And polkit before rtkit, which asks it on every
|
||||
/// request.
|
||||
#[test]
|
||||
fn realtime_audio_is_set_up_before_the_audio_server() {
|
||||
let at = |name: &str| {
|
||||
STACK
|
||||
.iter()
|
||||
.position(|s| s.name == name)
|
||||
.unwrap_or_else(|| panic!("{name} is not in the table"))
|
||||
};
|
||||
assert!(at("dbus-system") < at("polkitd"));
|
||||
assert!(at("polkitd") < at("rtkit"));
|
||||
assert!(at("rtkit") < at("pipewire"));
|
||||
}
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -21,6 +21,7 @@ static SLEEPERS: &[Service] = &[
|
||||
required: true,
|
||||
umask: None,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
Service {
|
||||
name: "second-sleeper",
|
||||
@@ -31,6 +32,7 @@ static SLEEPERS: &[Service] = &[
|
||||
required: true,
|
||||
umask: None,
|
||||
ready: None,
|
||||
ready_name: None,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+11
-1
@@ -311,9 +311,17 @@ RUN pacman-key --init
|
||||
# The second was found by the check further down rather than by reading the
|
||||
# list — dropping a package takes its dependency tree with it, and the part of
|
||||
# that tree something else was quietly using is not visible from here.
|
||||
# Created before the install, with the numbers the distribution reserves for them,
|
||||
# because the packages hand ownership of a few directories to these names and
|
||||
# the hook that would have made the users goes with systemd. `polkit` is here
|
||||
# for `rtkit`, which will not grant realtime priority to anything without it;
|
||||
# see the services table in `nesinit`.
|
||||
RUN groupadd -r -g 102 polkitd && useradd -r -u 102 -g polkitd -d / -s /usr/bin/nologin polkitd && \
|
||||
groupadd -r -g 133 rtkit && useradd -r -u 133 -g rtkit -d / -s /usr/bin/nologin rtkit
|
||||
|
||||
RUN pacman -Syu --noconfirm --needed \
|
||||
--assume-installed mesa --assume-installed opengl-driver \
|
||||
dbus \
|
||||
dbus polkit rtkit \
|
||||
iptables iproute2 \
|
||||
libglvnd libdrm libepoxy libxxf86vm libinput wayland \
|
||||
expat zlib llvm-libs lm_sensors elfutils libva shaderc vulkan-icd-loader \
|
||||
@@ -558,6 +566,7 @@ RUN failed=0; \
|
||||
/usr/bin/nesgamepad /usr/lib/libnescapture_layer.so /usr/lib/libSDL2-2.0.so.0 \
|
||||
/usr/lib/libVkLayer_nvgpu.so \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire \
|
||||
/usr/lib/polkit-1/polkitd /usr/lib/rtkit-daemon \
|
||||
/usr/bin/wireplumber /usr/bin/ip \
|
||||
/usr/lib/libgallium-*.so /usr/lib/libEGL_mesa.so.0 \
|
||||
/usr/lib/libvulkan_*.so /usr/lib/dri/*.so /usr/lib/gbm/*.so; do \
|
||||
@@ -579,6 +588,7 @@ RUN failed=0; \
|
||||
RUN for required in /usr/bin/nesinit /usr/bin/nescope /usr/bin/neshub /usr/bin/neswire \
|
||||
/usr/bin/nesgamepad /usr/lib/libSDL2-2.0.so.0 \
|
||||
/usr/bin/dbus-daemon /usr/bin/pipewire \
|
||||
/usr/lib/polkit-1/polkitd /usr/lib/rtkit-daemon \
|
||||
/usr/bin/wireplumber /usr/bin/ip /usr/bin/python3 /usr/bin/umu-run; do \
|
||||
test -x "$required" || { echo "the image is missing $required" >&2; exit 1; }; \
|
||||
done
|
||||
|
||||
Reference in New Issue
Block a user